Skip to content

Commit 021580f

Browse files
authored
Add /clear and /new commands (#256)
1 parent 2b4c539 commit 021580f

7 files changed

Lines changed: 319 additions & 3 deletions

File tree

agent/context_manager/manager.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,13 @@ def __init__(
184184
hf_token: str | None = None,
185185
local_mode: bool = False,
186186
):
187+
self.prompt_file_suffix = prompt_file_suffix
188+
self.tool_specs = tool_specs or []
189+
self.hf_token = hf_token
190+
self.local_mode = local_mode
187191
self.system_prompt = self._load_system_prompt(
188-
tool_specs or [],
189-
prompt_file_suffix="system_prompt_v3.yaml",
192+
self.tool_specs,
193+
prompt_file_suffix=self.prompt_file_suffix,
190194
hf_token=hf_token,
191195
local_mode=local_mode,
192196
)
@@ -203,6 +207,30 @@ def __init__(
203207
self.items: list[Message] = [Message(role="system", content=self.system_prompt)]
204208
self.on_message_added = None
205209

210+
def refresh_system_prompt(
211+
self,
212+
*,
213+
tool_specs: list[dict[str, Any]] | None = None,
214+
hf_token: str | None = None,
215+
local_mode: bool | None = None,
216+
) -> Message:
217+
"""Re-render the system prompt and return it as a system message."""
218+
if tool_specs is not None:
219+
self.tool_specs = tool_specs
220+
if hf_token is not None:
221+
self.hf_token = hf_token
222+
if local_mode is not None:
223+
self.local_mode = local_mode
224+
self.system_prompt = self._load_system_prompt(
225+
self.tool_specs,
226+
prompt_file_suffix=getattr(
227+
self, "prompt_file_suffix", "system_prompt_v3.yaml"
228+
),
229+
hf_token=getattr(self, "hf_token", None),
230+
local_mode=getattr(self, "local_mode", False),
231+
)
232+
return Message(role="system", content=self.system_prompt)
233+
206234
def _load_system_prompt(
207235
self,
208236
tool_specs: list[dict[str, Any]],

agent/core/agent_loop.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1756,6 +1756,19 @@ async def undo(session: Session) -> None:
17561756
logger.warning("Undo: no user message found to remove")
17571757
await session.send_event(Event(event_type="undo_complete"))
17581758

1759+
@staticmethod
1760+
async def new_conversation(session: Session, *, clear_screen: bool = False) -> None:
1761+
"""Start a fresh conversation inside the active runtime."""
1762+
try:
1763+
result = session.start_new_conversation()
1764+
except Exception as e:
1765+
await session.send_event(
1766+
Event(event_type="error", data={"error": f"New chat failed: {e}"})
1767+
)
1768+
return
1769+
result["clear_screen"] = clear_screen
1770+
await session.send_event(Event(event_type="new_complete", data=result))
1771+
17591772
@staticmethod
17601773
async def resume(session: Session, path: str) -> None:
17611774
"""Reload context from a saved session log into the active session."""
@@ -2058,6 +2071,11 @@ async def process_submission(session: Session, submission) -> bool:
20582071
await Handlers.undo(session)
20592072
return True
20602073

2074+
if op.op_type == OpType.NEW:
2075+
clear_screen = bool((op.data or {}).get("clear_screen"))
2076+
await Handlers.new_conversation(session, clear_screen=clear_screen)
2077+
return True
2078+
20612079
if op.op_type == OpType.RESUME:
20622080
path = op.data.get("path") if op.data else None
20632081
if path:

agent/core/session.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from pathlib import Path
1212
from typing import Any, Optional
1313

14+
from litellm import Message
15+
1416
from agent.config import Config
1517
from agent.context_manager.manager import ContextManager
1618
from agent.messaging.gateway import NotificationGateway
@@ -62,6 +64,7 @@ class OpType(Enum):
6264
INTERRUPT = "interrupt"
6365
UNDO = "undo"
6466
COMPACT = "compact"
67+
NEW = "new"
6568
RESUME = "resume"
6669
SHUTDOWN = "shutdown"
6770

@@ -376,6 +379,82 @@ def increment_turn(self) -> None:
376379
"""Increment turn counter (called after each user interaction)"""
377380
self.turn_count += 1
378381

382+
def start_new_conversation(self) -> dict[str, Any]:
383+
"""Rotate this runtime into a fresh conversation.
384+
385+
The tool router, model/config choices, user identity, and external
386+
resources stay attached to the CLI process. Conversation-specific state
387+
gets reset so later saves do not merge with the prior chat. Warm runtime
388+
resources such as the sandbox, in-flight job tracking, and probed
389+
model-effort cache are deliberately preserved.
390+
"""
391+
previous_session_id = self.session_id
392+
previous_turn_count = self.turn_count
393+
previous_message_count = len(self.context_manager.items)
394+
previous_non_system_count = sum(
395+
1
396+
for item in self.context_manager.items
397+
if getattr(item, "role", None) != "system"
398+
)
399+
400+
saved_path: str | None = None
401+
if self.config.save_sessions and previous_non_system_count:
402+
saved_path = self.save_and_upload_detached(self.config.session_dataset_repo)
403+
404+
from agent.tools.plan_tool import reset_current_plan
405+
406+
self.current_plan = []
407+
reset_current_plan()
408+
409+
system_msg = self._fresh_system_message()
410+
self.context_manager.items = [system_msg] if system_msg is not None else []
411+
self.context_manager.running_context_usage = 0
412+
413+
self.session_id = str(uuid.uuid4())
414+
self.session_start_time = datetime.now().isoformat()
415+
self.turn_count = 0
416+
self.last_auto_save_turn = 0
417+
self.logged_events = []
418+
self._local_save_path = None
419+
self._last_heartbeat_ts = None
420+
self.pending_approval = None
421+
self.auto_approval_estimated_spend_usd = 0.0
422+
self.reset_cancel()
423+
424+
# Previous-session metadata is intentionally included for event
425+
# consumers and telemetry, even though the CLI currently prints only
426+
# the optional save path.
427+
return {
428+
"session_id": self.session_id,
429+
"previous_session_id": previous_session_id,
430+
"previous_turn_count": previous_turn_count,
431+
"previous_message_count": previous_message_count,
432+
"saved_path": saved_path,
433+
}
434+
435+
def _fresh_system_message(self) -> Message | None:
436+
existing = (
437+
self.context_manager.items[0]
438+
if self.context_manager.items
439+
and getattr(self.context_manager.items[0], "role", None) == "system"
440+
else None
441+
)
442+
refresh = getattr(self.context_manager, "refresh_system_prompt", None)
443+
if refresh is None:
444+
return existing
445+
try:
446+
tool_specs = (
447+
self.tool_router.get_tool_specs_for_llm() if self.tool_router else []
448+
)
449+
return refresh(
450+
tool_specs=tool_specs,
451+
hf_token=self.hf_token,
452+
local_mode=self.local_mode,
453+
)
454+
except Exception as e:
455+
logger.warning("Failed to refresh system prompt for new chat: %s", e)
456+
return existing
457+
379458
async def auto_save_if_needed(self) -> None:
380459
"""Check if auto-save should trigger and save if so (completely non-blocking)"""
381460
if not self.config.save_sessions:

agent/main.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import logging
1313
import os
1414
import signal
15+
import subprocess
1516
import sys
1617
import time
1718
from dataclasses import dataclass
@@ -195,6 +196,14 @@ def _create_rich_console():
195196
return get_console()
196197

197198

199+
def _clear_terminal() -> None:
200+
command = ["cmd", "/c", "cls"] if os.name == "nt" else ["clear"]
201+
try:
202+
subprocess.run(command, check=False)
203+
except OSError:
204+
pass
205+
206+
198207
class _ThinkingShimmer:
199208
"""Animated shiny/shimmer thinking indicator — a bright gradient sweeps across the text."""
200209

@@ -398,6 +407,18 @@ def _cancel_event():
398407
elif event.event_type == "undo_complete":
399408
console.print("[dim]Undone.[/dim]")
400409
turn_complete_event.set()
410+
elif event.event_type == "new_complete":
411+
data = event.data or {}
412+
if data.get("clear_screen"):
413+
_clear_terminal()
414+
saved_path = data.get("saved_path")
415+
if saved_path:
416+
console.print(
417+
f"[dim]Started new chat. Prior chat saved to {saved_path}.[/dim]"
418+
)
419+
else:
420+
console.print("[dim]Started new chat.[/dim]")
421+
turn_complete_event.set()
401422
elif event.event_type == "resume_complete":
402423
data = event.data or {}
403424
path = data.get("path", "?")
@@ -902,6 +923,20 @@ async def _handle_slash_command(
902923
operation=Operation(op_type=OpType.COMPACT),
903924
)
904925

926+
if command in {"/new", "/clear"}:
927+
session = session_holder[0] if session_holder else None
928+
if session is None:
929+
get_console().print("[bold red]No active session to reset.[/bold red]")
930+
return None
931+
submission_id[0] += 1
932+
return Submission(
933+
id=f"sub_{submission_id[0]}",
934+
operation=Operation(
935+
op_type=OpType.NEW,
936+
data={"clear_screen": command == "/clear"},
937+
),
938+
)
939+
905940
if command == "/resume":
906941
session = session_holder[0] if session_holder else None
907942
if session is None:
@@ -1109,7 +1144,7 @@ async def main(model: str | None = None, sandbox_tools: bool = False):
11091144
"""Interactive chat with the agent"""
11101145

11111146
# Clear screen
1112-
os.system("clear" if os.name != "nt" else "cls")
1147+
_clear_terminal()
11131148

11141149
# Create prompt session for input (needed early for token prompt)
11151150
prompt_session = PromptSession()

agent/tools/plan_tool.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
_current_plan: List[Dict[str, str]] = []
1010

1111

12+
def reset_current_plan() -> None:
13+
"""Clear the CLI-visible in-memory plan."""
14+
global _current_plan
15+
16+
_current_plan = []
17+
18+
1219
class PlanTool:
1320
"""Tool for managing a list of todos with status tracking."""
1421

agent/utils/terminal_display.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,8 @@ def print_yolo_approve(count: int) -> None:
454454

455455
HELP_ROWS: tuple[tuple[str, str, str], ...] = (
456456
("/help", "", "Show this help"),
457+
("/new", "", "Start a fresh chat"),
458+
("/clear", "", "Clear terminal and start fresh"),
457459
("/undo", "", "Undo last turn"),
458460
("/compact", "", "Compact context window"),
459461
("/resume", "[index|id|path]", "Pick up from ./session_logs"),

0 commit comments

Comments
 (0)