core/cli: catalog select, session state, tag-aware yolo

This commit is contained in:
2026-07-24 14:56:48 +08:00
parent 15dde7cdaf
commit 2f6133f685
2 changed files with 52 additions and 17 deletions
+10 -5
View File
@@ -323,15 +323,20 @@ async def _run_chat( # noqa: C901, PLR0912, PLR0915 — chat orchestration
return EXIT_OK return EXIT_OK
finally: finally:
await memory.close() await memory.close()
from plyngent.tools.process.pty_session import PtyManager
from plyngent.tools.temp_workspace import cleanup_temporary_workspaces
PtyManager.close_all()
from plyngent.tools.workspace import clear_policy_allowed_commands, set_policy_confirm_hook from plyngent.tools.workspace import clear_policy_allowed_commands, set_policy_confirm_hook
set_policy_confirm_hook(None) set_policy_confirm_hook(None)
clear_policy_allowed_commands() clear_policy_allowed_commands()
_ = cleanup_temporary_workspaces() # PTY + temp workspace cleanup via instance shutdown when state exists.
state_obj = locals().get("state")
if isinstance(state_obj, ReplState):
await state_obj.instance_state.shutdown()
else:
from plyngent.tools.process.pty_session import PtyManager
from plyngent.tools.temp_workspace import cleanup_temporary_workspaces
PtyManager.close_all()
_ = cleanup_temporary_workspaces()
def _configure_logging(level: str) -> None: def _configure_logging(level: str) -> None:
+42 -12
View File
@@ -18,7 +18,13 @@ from plyngent.cli.models_source import (
) )
from plyngent.memory.database.store import normalize_workspace from plyngent.memory.database.store import normalize_workspace
from plyngent.runtime import create_client from plyngent.runtime import create_client
from plyngent.tools import DEFAULT_TOOLS, set_todo_stack, set_workspace_root from plyngent.tools import (
InstanceState,
SessionState,
default_tool_definitions,
set_todo_stack,
set_workspace_root,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Sequence from collections.abc import Sequence
@@ -58,6 +64,8 @@ class ReplState:
agent: ChatAgent = field(init=False) agent: ChatAgent = field(init=False)
session_id: int | None = None session_id: int | None = None
todo_stack: TodoStack = field(default_factory=TodoStack) todo_stack: TodoStack = field(default_factory=TodoStack)
instance_state: InstanceState = field(default_factory=InstanceState)
session_state: SessionState = field(default_factory=SessionState)
_todo_persist_tasks: set[object] = field(default_factory=set, init=False, repr=False) _todo_persist_tasks: set[object] = field(default_factory=set, init=False, repr=False)
# Session ids for Tab complete (updated when listing/creating/resuming). # Session ids for Tab complete (updated when listing/creating/resuming).
_session_id_cache: list[int] = field(default_factory=list, init=False, repr=False) _session_id_cache: list[int] = field(default_factory=list, init=False, repr=False)
@@ -71,6 +79,8 @@ class ReplState:
# DeepSeek client uses a compatible but distinct param type; treat as ChatClient. # DeepSeek client uses a compatible but distinct param type; treat as ChatClient.
self.client = cast("ChatClient", cast("object", create_client(self.provider))) self.client = cast("ChatClient", cast("object", create_client(self.provider)))
self.workspace = Path(self.workspace).expanduser().resolve() self.workspace = Path(self.workspace).expanduser().resolve()
self.instance_state.workspace_root = self.workspace
self.session_state.todo = self.todo_stack
self.agent = self._make_agent() self.agent = self._make_agent()
self.sync_display_flags() self.sync_display_flags()
self._bind_todo_tools() self._bind_todo_tools()
@@ -99,11 +109,17 @@ class ReplState:
return self.effective_yolo() == "off" return self.effective_yolo() == "off"
def set_yolo(self, mode: YoloMode) -> None: def set_yolo(self, mode: YoloMode) -> None:
"""Set YOLO mode; rebuild tool registry when soft-confirm hooks change.""" """Set YOLO mode; update registry YOLO bit (and rebuild if needed)."""
prev = self.soft_confirm_enabled() prev = self.effective_yolo()
self.yolo = mode self.yolo = mode
if prev != self.soft_confirm_enabled(): if prev != mode:
self.rebuild_client() # Tag-aware confirm: YOLO only auto-approves YOLO-tagged tools.
if hasattr(self, "agent") and self.agent.tools is not None:
self.agent.tools.set_yolo(enabled=mode != "off")
else:
self.rebuild_client()
if mode == "off":
self.session_state.clear_grants()
def expire_yolo_once(self, *, quiet: bool = False) -> None: def expire_yolo_once(self, *, quiet: bool = False) -> None:
"""If mode is ``once``, drop back to ``off`` after a user turn.""" """If mode is ``once``, drop back to ``off`` after a user turn."""
@@ -131,7 +147,12 @@ class ReplState:
self._todo_persist_tasks.add(task) self._todo_persist_tasks.add(task)
task.add_done_callback(self._todo_persist_tasks.discard) task.add_done_callback(self._todo_persist_tasks.discard)
self.session_state.todo = self.todo_stack
self.session_state.session_id = self.session_id
set_todo_stack(self.todo_stack, on_change=on_change) set_todo_stack(self.todo_stack, on_change=on_change)
if hasattr(self, "agent") and self.agent.tools is not None:
self.agent.tools.set_session_state(self.session_state)
self.agent.tools.set_instance_state(self.instance_state)
async def persist_todo_stack(self) -> None: async def persist_todo_stack(self) -> None:
"""Write the in-memory todo stack to the active session row.""" """Write the in-memory todo stack to the active session row."""
@@ -159,13 +180,18 @@ class ReplState:
from plyngent.cli.limits import prompt_confirm_tool_async from plyngent.cli.limits import prompt_confirm_tool_async
from plyngent.tools.danger import classify_danger from plyngent.tools.danger import classify_danger
if self.soft_confirm_enabled(): tools = default_tool_definitions(surface="local")
return ToolRegistry( yolo = self.effective_yolo() != "off"
list(DEFAULT_TOOLS), # Always attach soft-confirm path so non-YOLO tools still prompt under YOLO mode.
danger=classify_danger, return ToolRegistry(
on_confirm=prompt_confirm_tool_async, tools,
) danger=classify_danger,
return ToolRegistry(list(DEFAULT_TOOLS)) on_confirm=prompt_confirm_tool_async,
yolo=yolo,
auto_bind_state=True,
instance_state=self.instance_state,
session_state=self.session_state,
)
def _make_agent(self) -> ChatAgent: def _make_agent(self) -> ChatAgent:
from plyngent.cli.limits import prompt_continue_limit_async from plyngent.cli.limits import prompt_continue_limit_async
@@ -370,7 +396,10 @@ class ReplState:
msg = f"workspace is not a directory: {resolved}" msg = f"workspace is not a directory: {resolved}"
raise ValueError(msg) raise ValueError(msg)
self.workspace = resolved self.workspace = resolved
self.instance_state.workspace_root = resolved
_ = set_workspace_root(resolved) _ = set_workspace_root(resolved)
if hasattr(self, "agent") and self.agent.tools is not None:
self.agent.tools.set_instance_state(self.instance_state)
def _apply_session_workspace(self, row: SessionRow) -> None: def _apply_session_workspace(self, row: SessionRow) -> None:
"""Bind tools/REPL workspace to the session's directory when set.""" """Bind tools/REPL workspace to the session's directory when set."""
@@ -476,6 +505,7 @@ class ReplState:
if session.sid not in self._session_id_cache: if session.sid not in self._session_id_cache:
self._session_id_cache.insert(0, session.sid) self._session_id_cache.insert(0, session.sid)
self.todo_stack = TodoStack() self.todo_stack = TodoStack()
self.session_state = SessionState(session_id=session.sid, todo=self.todo_stack)
self.agent = self._make_agent() self.agent = self._make_agent()
self._bind_todo_tools() self._bind_todo_tools()