Skip to content

Commit 1af52f1

Browse files
committed
Merge branch 'main' into rel-1.7.0
2 parents c5870fd + 8246a04 commit 1af52f1

File tree

12 files changed

+353
-42
lines changed

12 files changed

+353
-42
lines changed

openhands_cli/acp_impl/agent.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,19 @@ def get_session_mode_state(current_mode: ConfirmationMode) -> SessionModeState:
9191
class OpenHandsACPAgent(ACPAgent):
9292
"""OpenHands Agent Client Protocol implementation."""
9393

94-
def __init__(self, conn: Client, initial_confirmation_mode: ConfirmationMode):
94+
def __init__(
95+
self,
96+
conn: Client,
97+
initial_confirmation_mode: ConfirmationMode,
98+
resume_conversation_id: str | None = None,
99+
):
95100
"""Initialize the OpenHands ACP agent.
96101
97102
Args:
98103
conn: ACP connection for sending notifications
99104
initial_confirmation_mode: Default confirmation mode for new sessions
105+
resume_conversation_id: Optional conversation ID to resume when a new
106+
session is created (used with --resume flag)
100107
"""
101108
self._conn = conn
102109
# Cache of active conversations to preserve state (pause, confirmation, etc.)
@@ -106,10 +113,14 @@ def __init__(self, conn: Client, initial_confirmation_mode: ConfirmationMode):
106113
self._running_tasks: dict[str, asyncio.Task] = {}
107114
# Default confirmation mode for new sessions
108115
self._initial_confirmation_mode: ConfirmationMode = initial_confirmation_mode
116+
# Conversation ID to resume (from --resume flag)
117+
self._resume_conversation_id: str | None = resume_conversation_id
109118
logger.info(
110119
f"OpenHands ACP Agent initialized with "
111120
f"confirmation mode: {initial_confirmation_mode}"
112121
)
122+
if resume_conversation_id:
123+
logger.info(f"Will resume conversation: {resume_conversation_id}")
113124

114125
async def _cmd_confirm(self, session_id: str, argument: str) -> str:
115126
"""Handle /confirm command.
@@ -362,8 +373,22 @@ async def new_session(
362373
mcp_servers: list[Any],
363374
**_kwargs: Any,
364375
) -> NewSessionResponse:
365-
"""Create a new conversation session."""
366-
session_id = str(uuid.uuid4())
376+
"""Create a new conversation session.
377+
378+
If --resume was used when starting the ACP server, the first new_session
379+
call will use the specified conversation ID instead of generating a new one.
380+
When resuming, historic events are replayed to the client.
381+
"""
382+
# Use resume_conversation_id if provided (from --resume flag)
383+
# Only use it once, then clear it
384+
is_resuming = False
385+
if self._resume_conversation_id:
386+
session_id = self._resume_conversation_id
387+
self._resume_conversation_id = None
388+
is_resuming = True
389+
logger.info(f"Resuming conversation: {session_id}")
390+
else:
391+
session_id = str(uuid.uuid4())
367392

368393
try:
369394
# Convert ACP MCP servers to Agent format
@@ -377,7 +402,7 @@ async def new_session(
377402

378403
# Create conversation and cache it for future operations
379404
# This reuses the same pattern as openhands --resume
380-
_ = self._get_or_create_conversation(
405+
conversation = self._get_or_create_conversation(
381406
session_id=session_id,
382407
working_dir=working_dir,
383408
mcp_servers=mcp_servers_dict,
@@ -389,15 +414,27 @@ async def new_session(
389414
await self._send_available_commands(session_id)
390415

391416
# Get current confirmation mode for this session
392-
conversation = self._active_sessions[session_id]
393417
current_mode = get_confirmation_mode_from_conversation(conversation)
394418

395-
# Return response with modes
396-
return NewSessionResponse(
419+
# Build response first (before streaming events)
420+
response = NewSessionResponse(
397421
session_id=session_id,
398422
modes=get_session_mode_state(current_mode),
399423
)
400424

425+
# If resuming, replay historic events to the client
426+
# This ensures the ACP client sees the full conversation history
427+
if is_resuming and conversation.state.events:
428+
logger.info(
429+
f"Replaying {len(conversation.state.events)} historic events "
430+
f"for resumed session {session_id}"
431+
)
432+
subscriber = EventSubscriber(session_id, self._conn)
433+
for event in conversation.state.events:
434+
await subscriber(event)
435+
436+
return response
437+
401438
except MissingAgentSpec as e:
402439
logger.error(f"Agent not configured: {e}")
403440
raise RequestError.internal_error(
@@ -695,21 +732,28 @@ async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
695732

696733
async def run_acp_server(
697734
initial_confirmation_mode: ConfirmationMode = "always-ask",
735+
resume_conversation_id: str | None = None,
698736
) -> None:
699737
"""Run the OpenHands ACP server.
700738
701739
Args:
702740
initial_confirmation_mode: Default confirmation mode for new sessions
741+
resume_conversation_id: Optional conversation ID to resume when a new
742+
session is created
703743
"""
704744
logger.info(
705745
f"Starting OpenHands ACP server with confirmation mode: "
706746
f"{initial_confirmation_mode}..."
707747
)
748+
if resume_conversation_id:
749+
logger.info(f"Will resume conversation: {resume_conversation_id}")
708750

709751
reader, writer = await stdio_streams()
710752

711753
def create_agent(conn: Client) -> OpenHandsACPAgent:
712-
return OpenHandsACPAgent(conn, initial_confirmation_mode)
754+
return OpenHandsACPAgent(
755+
conn, initial_confirmation_mode, resume_conversation_id
756+
)
713757

714758
AgentSideConnection(create_agent, writer, reader)
715759

openhands_cli/argparsers/acp_parser.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import argparse
22

3-
from openhands_cli.argparsers.util import add_confirmation_mode_args
3+
from openhands_cli.argparsers.util import add_confirmation_mode_args, add_resume_args
44

55

66
def add_acp_parser(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
@@ -13,6 +13,9 @@ def add_acp_parser(subparsers: argparse._SubParsersAction) -> argparse.ArgumentP
1313
),
1414
)
1515

16+
# Resume arguments (same as main parser)
17+
add_resume_args(acp_parser)
18+
1619
# ACP confirmation mode options (mutually exclusive)
1720
acp_confirmation_group = acp_parser.add_mutually_exclusive_group()
1821
add_confirmation_mode_args(acp_confirmation_group)

openhands_cli/argparsers/main_parser.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from openhands_cli.argparsers.cloud_parser import add_cloud_parser
99
from openhands_cli.argparsers.mcp_parser import add_mcp_parser
1010
from openhands_cli.argparsers.serve_parser import add_serve_parser
11-
from openhands_cli.argparsers.utils import add_confirmation_mode_args
11+
from openhands_cli.argparsers.util import add_confirmation_mode_args, add_resume_args
1212
from openhands_cli.argparsers.web_parser import add_web_parser
1313

1414

@@ -72,19 +72,7 @@ def create_main_parser() -> argparse.ArgumentParser:
7272
)
7373

7474
# CLI arguments at top level (default mode)
75-
parser.add_argument(
76-
"--resume",
77-
type=str,
78-
nargs="?",
79-
const="",
80-
help="Conversation ID to resume. If no ID provided, shows list of recent "
81-
"conversations",
82-
)
83-
parser.add_argument(
84-
"--last",
85-
action="store_true",
86-
help="Resume the most recent conversation (use with --resume)",
87-
)
75+
add_resume_args(parser)
8876
parser.add_argument(
8977
"--exp",
9078
action="store_true",

openhands_cli/argparsers/util.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,24 @@ def add_confirmation_mode_args(
2222
"(only confirm LLM-predicted high-risk actions)"
2323
),
2424
)
25+
26+
27+
def add_resume_args(parser: argparse.ArgumentParser) -> None:
28+
"""Add resume-related arguments to a parser.
29+
30+
Args:
31+
parser: The argument parser to add resume arguments to
32+
"""
33+
parser.add_argument(
34+
"--resume",
35+
type=str,
36+
nargs="?",
37+
const="",
38+
help="Conversation ID to resume. If no ID provided, shows list of recent "
39+
"conversations",
40+
)
41+
parser.add_argument(
42+
"--last",
43+
action="store_true",
44+
help="Resume the most recent conversation (use with --resume)",
45+
)

openhands_cli/simple_main.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,18 @@ def main() -> None:
124124
elif args.llm_approve:
125125
confirmation_mode = "llm-approve"
126126

127-
asyncio.run(run_acp_server(initial_confirmation_mode=confirmation_mode))
127+
# Handle resume logic for ACP (same as main command)
128+
resume_id = handle_resume_logic(args)
129+
if resume_id is None and (args.last or args.resume == ""):
130+
# Either showed conversation list or had an error
131+
return
132+
133+
asyncio.run(
134+
run_acp_server(
135+
initial_confirmation_mode=confirmation_mode,
136+
resume_conversation_id=resume_id,
137+
)
138+
)
128139

129140
elif args.command == "login":
130141
from openhands_cli.auth.login_command import run_login_command

openhands_cli/stores/agent_store.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
WORK_DIR,
2121
)
2222
from openhands_cli.mcp.mcp_utils import list_enabled_servers
23-
from openhands_cli.utils import get_llm_metadata, should_set_litellm_extra_body
23+
from openhands_cli.utils import (
24+
get_llm_metadata,
25+
get_os_description,
26+
should_set_litellm_extra_body,
27+
)
2428

2529

2630
class AgentStore:
@@ -40,9 +44,16 @@ def load(self, session_id: str | None = None) -> Agent | None:
4044
# Load skills from user directories and project-specific directories
4145
skills = load_project_skills(WORK_DIR)
4246

47+
system_suffix = "\n".join(
48+
[
49+
f"Your current working directory is: {WORK_DIR}",
50+
f"User operating system: {get_os_description()}",
51+
]
52+
)
53+
4354
agent_context = AgentContext(
4455
skills=skills,
45-
system_message_suffix=f"You current working directory is: {WORK_DIR}",
56+
system_message_suffix=system_suffix,
4657
load_user_skills=True,
4758
load_public_skills=True,
4859
)
@@ -62,9 +73,12 @@ def load(self, session_id: str | None = None) -> Agent | None:
6273
}
6374
updated_llm = agent.llm.model_copy(update=llm_update)
6475

65-
condenser_updates = {}
76+
# Always create a fresh condenser with current defaults if condensation
77+
# is enabled. This ensures users get the latest condenser settings
78+
# (e.g., max_size, keep_first) without needing to reconfigure.
79+
condenser = None
6680
if agent.condenser and isinstance(agent.condenser, LLMSummarizingCondenser):
67-
condenser_llm_update = {}
81+
condenser_llm_update: dict[str, Any] = {}
6882
if should_set_litellm_extra_body(agent.condenser.llm.model):
6983
condenser_llm_update["litellm_extra_body"] = {
7084
"metadata": get_llm_metadata(
@@ -73,9 +87,10 @@ def load(self, session_id: str | None = None) -> Agent | None:
7387
session_id=session_id,
7488
)
7589
}
76-
condenser_updates["llm"] = agent.condenser.llm.model_copy(
90+
condenser_llm = agent.condenser.llm.model_copy(
7791
update=condenser_llm_update
7892
)
93+
condenser = LLMSummarizingCondenser(llm=condenser_llm)
7994

8095
# Update tools and context
8196
agent = agent.model_copy(
@@ -86,9 +101,7 @@ def load(self, session_id: str | None = None) -> Agent | None:
86101
if enabled_servers
87102
else {},
88103
"agent_context": agent_context,
89-
"condenser": agent.condenser.model_copy(update=condenser_updates)
90-
if agent.condenser
91-
else None,
104+
"condenser": condenser,
92105
}
93106
)
94107

openhands_cli/utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
import os
5+
import platform
56
from argparse import Namespace
67
from pathlib import Path
78
from typing import Any
@@ -15,6 +16,26 @@
1516
from openhands.tools.preset import get_default_agent
1617

1718

19+
def get_os_description() -> str:
20+
system = platform.system() or "Unknown"
21+
22+
if system == "Darwin":
23+
ver = platform.mac_ver()[0] or platform.release()
24+
return f"macOS {ver}".strip()
25+
26+
if system == "Windows":
27+
release, version, *_ = platform.win32_ver()
28+
if release and version:
29+
return f"Windows {release} ({version})"
30+
return "Windows"
31+
32+
if system == "Linux":
33+
kernel = platform.release()
34+
return f"Linux (kernel {kernel})" if kernel else "Linux"
35+
36+
return platform.platform() or system
37+
38+
1839
def should_set_litellm_extra_body(model_name: str) -> bool:
1940
"""
2041
Determine if litellm_extra_body should be set based on the model name.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ classifiers = [
1616
"Programming Language :: Python :: 3.13",
1717
]
1818
dependencies = [
19-
"openhands-sdk==1.7.1",
20-
"openhands-tools==1.7.1",
19+
"openhands-sdk==1.7.2",
20+
"openhands-tools==1.7.2",
2121
"prompt-toolkit>=3",
2222
"textual>=0.79.0",
2323
"typer>=0.17.4",

0 commit comments

Comments
 (0)