Skip to content

Commit 595f2d0

Browse files
authored
follow torvards-mode suggestion: integrate lock with state, and modify state in-place (#40)
1 parent 77023f5 commit 595f2d0

File tree

5 files changed

+232
-58
lines changed

5 files changed

+232
-58
lines changed

README.md

Lines changed: 163 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,167 @@
1-
# Prototype for OpenHands V1
2-
3-
This folder contains my tasks of completely refactor [OpenHands](https://github.com/All-Hands-AI/OpenHands) project V0 into the new V1 version. There's a lot of changes, including (non-exhausive):
4-
5-
- Switching from poetry to uv as package manager
6-
- better dependency management
7-
- include `--dev` group for development only
8-
- stricter pre-commit hooks `.pre-commit-config.yaml` that includes
9-
- type check through pyright
10-
- linting and formatter with `uv ruff`
11-
- cleaner architecture for how a tool works and how it is executed
12-
- read about how we define tools: [`openhands/core/runtime/tool.py`](openhands/core/runtime/tool.py)
13-
- read about how we define schema (input/output) for tools: [`openhands/core/runtime/schema.py`](openhands/core/runtime/schema.py)
14-
- read about patterns for how we define an executable tool:
15-
- read [openhands/core/runtime/tools/str_replace_editor/impl.py](openhands/core/runtime/tools/str_replace_editor/impl.py) for tool execute_fn
16-
- read [openhands/core/runtime/tools/str_replace_editor/definition.py](openhands/core/runtime/tools/str_replace_editor/definition.py) for how do we define a tool
17-
- read [openhands/core/runtime/tools/str_replace_editor/__init__.py](openhands/core/runtime/tools/str_replace_editor/__init__.py) for how we define each tool module
18-
- tools: `str_replace_editor`, `execute_bash`
19-
- minimal config (OpenHandsConfig, LLMConfig, MCPConfig): `openhands/core/config`
20-
- core set of LLM (w/o tests): `openhands/core/llm`
21-
- core set of microagent functionality (w/o full integration):
22-
- `openhands/core/context`: redesigned the triggering of microagents w.r.t. agents into the concept of two types context
23-
- EnvContext (triggered at the begining of a convo)
24-
- MessageContext (triggered at each user message)
25-
- `openhands-v1/openhands/core/microagents`: old code from V1 that loads microagents from folders, etc
26-
- minimal implementation of codeact agent: `openhands-v1/openhands/core/agenthub/codeact_agent`
27-
- ...
28-
29-
30-
**Check hello world example**
1+
# OpenHands Agent SDK
2+
3+
A clean, modular SDK for building AI agents with OpenHands. This project represents a complete architectural refactor from OpenHands V0, emphasizing simplicity, maintainability, and developer experience.
4+
5+
## Repository Structure
6+
7+
```
8+
agent-sdk/
9+
├── .github/
10+
│ └── workflows/ # CI/CD workflows
11+
│ ├── precommit.yml # Pre-commit hook validation
12+
│ └── tests.yml # Test execution pipeline
13+
├── .pre-commit-config.yaml # Pre-commit hooks configuration
14+
├── Makefile # Build and development commands
15+
├── README.md # This file
16+
├── pyproject.toml # Root project configuration
17+
├── uv.lock # Dependency lock file
18+
├── examples/
19+
│ └── hello_world.py # Getting started example
20+
├── openhands/ # Main SDK packages
21+
│ ├── core/ # Core SDK functionality
22+
│ │ ├── agent/ # Agent implementations
23+
│ │ │ ├── base.py # Base agent interface
24+
│ │ │ └── codeact_agent/ # CodeAct agent implementation
25+
│ │ ├── config/ # Configuration management
26+
│ │ │ ├── llm_config.py # LLM configuration
27+
│ │ │ └── mcp_config.py # MCP configuration
28+
│ │ ├── context/ # Context management system
29+
│ │ │ ├── env_context.py # Environment context
30+
│ │ │ ├── message_context.py # Message context
31+
│ │ │ ├── history.py # Conversation history
32+
│ │ │ ├── manager.py # Context manager
33+
│ │ │ ├── prompt.py # Prompt management
34+
│ │ │ └── microagents/ # Microagent system
35+
│ │ ├── conversation/ # Conversation management
36+
│ │ │ ├── conversation.py # Core conversation logic
37+
│ │ │ ├── serializer.py # Conversation serialization
38+
│ │ │ ├── state.py # Conversation state
39+
│ │ │ ├── types.py # Type definitions
40+
│ │ │ └── visualizer.py # Conversation visualization
41+
│ │ ├── llm/ # LLM integration layer
42+
│ │ │ ├── llm.py # Main LLM interface
43+
│ │ │ ├── message.py # Message handling
44+
│ │ │ ├── metadata.py # LLM metadata
45+
│ │ │ └── utils/ # LLM utilities
46+
│ │ ├── tool/ # Tool system
47+
│ │ │ ├── tool.py # Core tool interface
48+
│ │ │ ├── schema.py # Tool schema definitions
49+
│ │ │ └── builtins/ # Built-in tools
50+
│ │ ├── utils/ # Core utilities
51+
│ │ ├── logger.py # Logging configuration
52+
│ │ ├── pyproject.toml # Core package configuration
53+
│ │ └── tests/ # Unit tests for core
54+
│ └── tools/ # Tool implementations
55+
│ ├── execute_bash/ # Bash execution tool
56+
│ ├── str_replace_editor/ # String replacement editor
57+
│ ├── utils/ # Tool utilities
58+
│ ├── pyproject.toml # Tools package configuration
59+
│ └── tests/ # Unit tests for tools
60+
└── tests/ # Integration tests
61+
```
62+
63+
## Quick Start
64+
65+
```bash
66+
# Install dependencies
67+
make build
68+
69+
# Run hello world example
70+
uv run python examples/hello_world.py
71+
72+
# Run tests
73+
uv run pytest
74+
75+
# Run pre-commit hooks
76+
uv run pre-commit run --all-files
77+
```
78+
79+
## Development Guidelines
80+
81+
### Core Principles
82+
83+
This project follows principles of simplicity, pragmatism, and maintainability:
84+
85+
1. **Simplicity First**: If it needs more than 3 levels of indentation, redesign it
86+
2. **No Special Cases**: Good code eliminates edge cases through proper data structure design
87+
3. **Pragmatic Solutions**: Solve real problems, not imaginary ones
88+
4. **Never Break Userspace**: Backward compatibility is sacred
89+
90+
### Architecture Overview
91+
92+
The SDK is built around two core packages:
93+
94+
- **`openhands/core`**: Core SDK functionality (agents, LLM, context, conversation)
95+
- **`openhands/tools`**: Tool implementations (bash execution, file editing)
96+
97+
Each package is independently testable and deployable, with clear separation of concerns.
98+
99+
### Development Workflow
100+
101+
#### 1. Environment Setup
31102

32103
```bash
104+
# Initial setup
105+
make build
106+
107+
# Activate virtual environment (if needed)
108+
source .venv/bin/activate
109+
```
110+
111+
#### 2. Code Quality Standards
112+
113+
- **Type Checking**: All code must pass `pyright` type checking
114+
- **Linting**: Code must pass `ruff` linting and formatting
115+
- **Testing**: Maintain test coverage for new functionality
116+
- **Documentation**: Code should be self-documenting; avoid redundant comments
117+
118+
#### 3. Pre-commit Workflow
119+
120+
Before every commit:
121+
122+
```bash
123+
# Run pre-commit hooks on changed files
124+
uv run pre-commit run --files <filepath>
125+
126+
# Or run on all files
127+
uv run pre-commit run --all-files
128+
```
129+
130+
#### 4. Testing Strategy
131+
132+
**Unit Tests**: Located in package-specific test directories
133+
- `openhands/core/tests/` - Tests for core functionality
134+
- `openhands/tools/tests/` - Tests for tool implementations
135+
136+
**Integration Tests**: Located in root `tests/` directory
137+
- Tests that involve both core and tools packages
138+
139+
**Running Tests**:
140+
```bash
141+
# Run all tests
142+
uv run pytest
143+
144+
# Run specific test file
145+
uv run pytest openhands/core/tests/tool/test_tool.py
146+
147+
# Run with coverage
148+
uv run pytest --cov=openhands
149+
```
150+
151+
#### 5. Package Management
152+
153+
This project uses `uv` for dependency management:
154+
155+
```bash
156+
# Add a new dependency
157+
uv add package-name
158+
159+
# Add a development dependency
160+
uv add --dev package-name
161+
162+
# Update dependencies
163+
uv lock --upgrade
164+
165+
# Install from lock file
33166
uv sync
34-
uv run python examples/hello.py
35167
```

openhands/core/agent/base.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,15 @@ def init_state(
6262
state: ConversationState,
6363
initial_user_message: Message | None = None,
6464
on_event: ConversationCallbackType | None = None,
65-
) -> ConversationState:
65+
) -> None:
6666
"""Initialize the empty conversation state to prepare the agent for user messages.
6767
6868
Typically this involves:
6969
1. Adding system message
7070
2. Adding initial user messages with environment context
7171
(e.g., microagents, current working dir, etc)
72+
73+
NOTE: state will be mutated in-place.
7274
"""
7375
raise NotImplementedError("Subclasses must implement this method.")
7476

@@ -77,7 +79,7 @@ def step(
7779
self,
7880
state: ConversationState,
7981
on_event: ConversationCallbackType | None = None,
80-
) -> ConversationState:
82+
) -> None:
8183
"""Taking a step in the conversation.
8284
8385
Typically this involves:
@@ -87,5 +89,7 @@ def step(
8789
LLM calls (role="assistant") and tool results (role="tool")
8890
4.1 If conversation is finished, set state.agent_finished flag
8991
4.2 Otherwise, just return, Conversation will kick off the next step
92+
93+
NOTE: state will be mutated in-place.
9094
"""
9195
raise NotImplementedError("Subclasses must implement this method.")

openhands/core/agent/codeact_agent/codeact_agent.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def init_state(
4747
state: ConversationState,
4848
initial_user_message: Message | None = None,
4949
on_event: ConversationCallbackType | None = None,
50-
) -> ConversationState:
50+
) -> None:
5151
# TODO(openhands): we should add test to test this init_state will actually modify state in-place
5252
messages = state.history.messages
5353
if len(messages) == 0:
@@ -74,13 +74,12 @@ def init_state(
7474
if self.env_context and self.env_context.activated_microagents:
7575
for microagent in self.env_context.activated_microagents:
7676
state.history.microagent_activations.append((microagent.name, len(messages) - 1))
77-
return state
7877

7978
def step(
8079
self,
8180
state: ConversationState,
8281
on_event: ConversationCallbackType | None = None,
83-
) -> ConversationState:
82+
) -> None:
8483
# Get LLM Response (Action)
8584
_messages = self.llm.format_messages_for_llm(state.history.messages)
8685
logger.debug(f"Sending messages to LLM: {json.dumps(_messages, indent=2)}")
@@ -102,18 +101,21 @@ def step(
102101
tool_calls = [tool_call for tool_call in message.tool_calls if tool_call.type == "function"]
103102
assert len(tool_calls) > 0, "LLM returned tool calls but none are of type 'function'"
104103
for tool_call in tool_calls:
105-
state = self._handle_tool_call(tool_call, state, on_event)
104+
self._handle_tool_call(tool_call, state, on_event)
106105
else:
107106
logger.info("LLM produced a message response - awaits user input")
108107
state.agent_finished = True
109-
return state
110108

111109
def _handle_tool_call(
112110
self,
113111
tool_call: ChatCompletionMessageToolCall,
114112
state: ConversationState,
115113
on_event: Callable[[Message | ActionBase | ObservationBase], None] | None = None,
116-
) -> ConversationState:
114+
) -> None:
115+
"""Handle tool calls from the LLM.
116+
117+
NOTE: state will be mutated in-place.
118+
"""
117119
assert tool_call.type == "function"
118120
tool_name = tool_call.function.name
119121
assert tool_name is not None, "Tool call must have a name"
@@ -124,7 +126,7 @@ def _handle_tool_call(
124126
logger.error(err)
125127
state.history.messages.append(Message(role="user", content=[TextContent(text=err)]))
126128
state.agent_finished = True
127-
return state
129+
return
128130

129131
# Validate arguments
130132
try:
@@ -135,7 +137,7 @@ def _handle_tool_call(
135137
err = f"Error validating args {tool_call.function.arguments} for tool '{tool.name}': {e}"
136138
logger.error(err)
137139
state.history.messages.append(Message(role="tool", name=tool.name, tool_call_id=tool_call.id, content=[TextContent(text=err)]))
138-
return state
140+
return
139141

140142
# Execute actions!
141143
if tool.executor is None:
@@ -154,4 +156,3 @@ def _handle_tool_call(
154156
# Set conversation state
155157
if tool.name == FinishTool.name:
156158
state.agent_finished = True
157-
return state

openhands/core/conversation/conversation.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
if TYPE_CHECKING:
55
from openhands.core.agent import AgentBase
6-
from threading import RLock
76

87
from openhands.core.llm import Message
98
from openhands.core.logger import get_logger
@@ -39,27 +38,22 @@ def __init__(
3938
self.max_iteration_per_run = max_iteration_per_run
4039

4140
self.agent = agent
42-
self._agent_initialized = False
4341

44-
# Guarding the conversation state to prevent multiple
45-
# writers modify it at the same time
46-
self._lock = RLock()
4742
self.state = ConversationState()
4843

4944
def send_message(self, message: Message) -> None:
5045
"""Sending messages to the agent."""
51-
with self._lock:
52-
if not self._agent_initialized:
53-
# Prepare initial state
54-
self.state = self.agent.init_state(
46+
with self.state:
47+
if not self.state.agent_initialized:
48+
# mutate in place; agent must follow this contract
49+
self.agent.init_state(
5550
self.state,
5651
initial_user_message=message,
5752
on_event=self._on_event,
5853
)
59-
self._agent_initialized = True
54+
self.state.agent_initialized = True
6055
else:
61-
messages = self.state.history.messages
62-
messages.append(message)
56+
self.state.history.messages.append(message)
6357
if self._on_event:
6458
self._on_event(message)
6559

@@ -68,8 +62,14 @@ def run(self) -> None:
6862
iteration = 0
6963
while not self.state.agent_finished:
7064
logger.debug(f"Conversation run iteration {iteration}")
71-
with self._lock:
72-
self.state = self.agent.step(self.state, on_event=self._on_event)
65+
# TODO(openhands): we should add a testcase that test IF:
66+
# 1. a loop is running
67+
# 2. in a separate thread .send_message is called
68+
# and check will we be able to execute .send_message
69+
# BEFORE the .run loop finishes?
70+
with self.state:
71+
# step must mutate the SAME state object
72+
self.agent.step(self.state, on_event=self._on_event)
7373
iteration += 1
7474
if iteration >= self.max_iteration_per_run:
7575
break

0 commit comments

Comments
 (0)