Compare commits

..

25 Commits

Author SHA1 Message Date
openhands
bd7c1db423 Fix agent session tests to mock Memory component 2025-03-05 03:08:50 +00:00
openhands
145a4bdc81 Resolve merge conflicts with main branch 2025-02-28 07:46:18 +00:00
Engel Nyst
c25701fc1b add memory.py 2025-02-24 17:32:52 +01:00
Engel Nyst
bec05943a0 Merge branch 'main' of github.com:All-Hands-AI/OpenHands into enyst/retrieve-prompt 2025-02-24 17:32:00 +01:00
Engel Nyst
2c5018f529 Merge branch 'main' of github.com:All-Hands-AI/OpenHands into enyst/retrieve-prompt 2025-02-24 00:21:21 +01:00
Engel Nyst
d596fd2782 refactor info in the first user message to a recalled observation 2025-02-24 00:13:28 +01:00
Engel Nyst
21c2253634 fix disabled microagents 2025-02-23 21:20:23 +01:00
Engel Nyst
b95d54020c refactor prompt manager to manage the view, memory manages info retrieval 2025-02-23 21:11:00 +01:00
Engel Nyst
0e54bab56a rename to memory 2025-02-23 20:03:47 +01:00
Engel Nyst
bb5817cb56 rename main module to memory 2025-02-23 20:01:48 +01:00
Engel Nyst
f109a2ad95 rename memory to long term memory 2025-02-23 19:52:32 +01:00
Engel Nyst
956b3b4ab7 create memory 2025-02-23 00:23:41 +01:00
Engel Nyst
66781fc0f6 fix subscriber 2025-02-23 00:08:55 +01:00
Engel Nyst
143293db44 fix logic 2025-02-23 00:06:59 +01:00
Engel Nyst
c26185daf0 dont want to fight o1 right now, will revisit 2025-02-22 23:57:02 +01:00
Engel Nyst
16da353508 refactor prompt extensions 2025-02-22 23:39:32 +01:00
Engel Nyst
c21ddaf1f1 add recall action and observation 2025-02-22 22:00:52 +01:00
Engel Nyst
801b134c7f fix tests 2025-02-22 20:36:59 +01:00
Engel Nyst
5b063cc11b add tests 2025-02-22 20:18:34 +01:00
Engel Nyst
b1a18d5330 retrieve tokens usage for an event 2025-02-22 20:14:23 +01:00
Engel Nyst
38b5198c24 fix not initialized 2025-02-22 19:52:58 +01:00
Engel Nyst
dba25f5c46 clean up 2025-02-22 19:20:14 +01:00
Engel Nyst
c59abb5305 test accumulation 2025-02-22 19:04:12 +01:00
Engel Nyst
bd9fc5551b add response_id 2025-02-22 18:59:48 +01:00
Engel Nyst
d80c3767ae track used tokens 2025-02-22 18:48:35 +01:00
126 changed files with 3403 additions and 3959 deletions

View File

@@ -1,55 +0,0 @@
cff-version: 1.2.0
message: "If you use this software, please cite it using the following metadata."
title: "OpenHands: An Open Platform for AI Software Developers as Generalist Agents"
authors:
- family-names: Wang
given-names: Xingyao
- family-names: Li
given-names: Boxuan
- family-names: Song
given-names: Yufan
- family-names: Xu
given-names: Frank F.
- family-names: Tang
given-names: Xiangru
- family-names: Zhuge
given-names: Mingchen
- family-names: Pan
given-names: Jiayi
- family-names: Song
given-names: Yueqi
- family-names: Li
given-names: Bowen
- family-names: Singh
given-names: Jaskirat
- family-names: Tran
given-names: Hoang H.
- family-names: Li
given-names: Fuqiang
- family-names: Ma
given-names: Ren
- family-names: Zheng
given-names: Mingzhang
- family-names: Qian
given-names: Bill
- family-names: Shao
given-names: Yanjun
- family-names: Muennighoff
given-names: Niklas
- family-names: Zhang
given-names: Yizhe
- family-names: Hui
given-names: Binyuan
- family-names: Lin
given-names: Junyang
- family-names: Brennan
given-names: Robert
- family-names: Peng
given-names: Hao
- family-names: Ji
given-names: Heng
- family-names: Neubig
given-names: Graham
year: 2024
doi: "10.48550/arXiv.2407.16741"
url: "https://arxiv.org/abs/2407.16741"

View File

@@ -2,13 +2,12 @@
These are the procedures and guidelines on how issues are triaged in this repo by the maintainers.
## General
* All issues must be tagged with **enhancement**, **bug** or **troubleshooting/help**.
* Issues may be tagged with what it relates to (**agent quality**, **frontend**, **resolver**, etc.).
* Most issues must be tagged with **enhancement** or **bug**.
* Issues may be tagged with what it relates to (**backend**, **frontend**, **agent quality**, etc.).
## Severity
* **Low**: Minor issues or affecting single user.
* **Medium**: Affecting multiple users.
* **High**: High visibility issues or affecting many users.
* **Critical**: Affecting all users or potential security issues.
## Effort
@@ -19,14 +18,8 @@ These are the procedures and guidelines on how issues are triaged in this repo b
## Not Enough Information
* User is asked to provide more information (logs, how to reproduce, etc.) when the issue is not clear.
* If an issue is unclear and the author does not provide more information or respond to a request,
the issue may be closed as **not planned** (Usually after a week).
* If an issue is unclear and the author does not provide more information or respond to a request, the issue may be closed as **not planned** (Usually after a week).
## Multiple Requests/Fixes in One Issue
* These issues will be narrowed down to one request/fix so the issue is more easily tracked and fixed.
* Issues may be broken down into multiple issues if required.
## Stale and Auto Closures
* In order to keep a maintainable backlog, issues that have no activity within 30 days are automatically marked as **Stale**.
* If issues marked as **Stale** continue to have no activity for 7 more days, they will automatically be closed as not planned.
* Issues may be reopened by maintainers if deemed important.

View File

@@ -95,11 +95,6 @@ workspace_base = "./workspace"
# List of allowed file extensions for uploads
#file_uploads_allowed_extensions = [".*"]
# Whether to enable the default LLM summarizing condenser when no condenser is specified in config
# When true, a LLMSummarizingCondenserConfig will be used as the default condenser
# When false, a NoOpCondenserConfig (no summarization) will be used
#enable_default_condenser = true
#################################### LLM #####################################
# Configuration for LLM models (group name starts with 'llm')
# use 'llm' for the default LLM config
@@ -299,69 +294,6 @@ llm_config = 'gpt3'
# The security analyzer to use (For Headless / CLI only - In Web this is overridden by Session Init)
#security_analyzer = ""
#################################### Condenser #################################
# Condensers control how conversation history is managed and compressed when
# the context grows too large. Each agent uses one condenser configuration.
##############################################################################
[condenser]
# The type of condenser to use. Available options:
# - "noop": No condensing, keeps full history (default)
# - "observation_masking": Keeps full event structure but masks older observations
# - "recent": Keeps only recent events and discards older ones
# - "llm": Uses an LLM to summarize conversation history
# - "amortized": Intelligently forgets older events while preserving important context
# - "llm_attention": Uses an LLM to prioritize most relevant context
type = "noop"
# Examples for each condenser type (uncomment and modify as needed):
# 1. NoOp Condenser - No additional settings needed
#type = "noop"
# 2. Observation Masking Condenser
#type = "observation_masking"
# Number of most-recent events where observations will not be masked
#attention_window = 100
# 3. Recent Events Condenser
#type = "recent"
# Number of initial events to always keep (typically includes task description)
#keep_first = 1
# Maximum number of events to keep in history
#max_events = 100
# 4. LLM Summarizing Condenser
#type = "llm"
# Reference to an LLM config to use for summarization
#llm_config = "condenser"
# Number of initial events to always keep (typically includes task description)
#keep_first = 1
# Maximum size of history before triggering summarization
#max_size = 100
# 5. Amortized Forgetting Condenser
#type = "amortized"
# Number of initial events to always keep (typically includes task description)
#keep_first = 1
# Maximum size of history before triggering forgetting
#max_size = 100
# 6. LLM Attention Condenser
#type = "llm_attention"
# Reference to an LLM config to use for attention scoring
#llm_config = "condenser"
# Number of initial events to always keep (typically includes task description)
#keep_first = 1
# Maximum size of history before triggering attention mechanism
#max_size = 100
# Example of a custom LLM configuration for condensers that require an LLM
# If not provided, it falls back to the default LLM
#[llm.condenser]
#model = "gpt-4o"
#temperature = 0.1
#max_tokens = 1024
#################################### Eval ####################################
# Configuration for the evaluation, please refer to the specific evaluation
# plugin for the available options

View File

@@ -385,11 +385,6 @@ To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-
- Default: `false`
- Description: Use host network
- `runtime_binding_address`
- Type: `str`
- Default: `127.0.0.1`
- Description: The binding address for the runtime ports. It specifies which network interface on the host machine Docker should bind the runtime ports to.
### Linting and Plugins
- `enable_auto_lint`
- Type: `bool`

View File

@@ -84,36 +84,3 @@ docker run # ...
-e MODAL_API_TOKEN_ID="your-id" \
-e MODAL_API_TOKEN_SECRET="your-secret" \
```
## Daytona Runtime
Another option is using [Daytona](https://www.daytona.io/) as a runtime provider:
### Step 1: Retrieve Your Daytona API Key
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
2. Click **"Create Key"**.
3. Enter a name for your key and confirm the creation.
4. Once the key is generated, copy it.
### Step 2: Set Your API Key as an Environment Variable
Run the following command in your terminal, replacing `<your-api-key>` with the actual key you copied:
```bash
export DAYTONA_API_KEY="<your-api-key>"
```
This step ensures that OpenHands can authenticate with the Daytona platform when it runs.
### Step 3: Run OpenHands Locally Using Docker
To start the latest version of OpenHands on your machine, execute the following command in your terminal:
```bash
bash -i <(curl -sL https://get.daytona.io/openhands)
```
#### What This Command Does:
- Downloads the latest OpenHands release script.
- Runs the script in an interactive Bash session.
- Automatically pulls and runs the OpenHands container using Docker.
Once executed, OpenHands should be running locally and ready for use.
For more details and manual initialization, view the entire [README.md](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/impl/daytona/README.md)

View File

@@ -5,6 +5,9 @@ export function Demo() {
const videoRef = React.useRef<HTMLVideoElement>(null);
return (
<div
style={{ paddingBottom: "10px", paddingTop: "10px", textAlign: "center" }}
>
<video
playsInline
autoPlay={true}
@@ -17,5 +20,6 @@ export function Demo() {
>
<source src="img/teaser.mp4" type="video/mp4"></source>
</video>
</div>
);
}

View File

@@ -1,5 +1,6 @@
.demo {
width: 100%;
padding: 30px;
max-width: 800px;
text-align: center;
border-radius: 40px;

View File

@@ -17,29 +17,6 @@ export function HomepageHeader() {
<p className="header-subtitle">{siteConfig.tagline}</p>
<div style={{
textAlign: 'center',
fontSize: '1.2rem',
maxWidth: '800px',
margin: '0 auto',
padding: '0rem 0rem 1rem'
}}>
<p style={{ margin: '0' }}>
Use AI to tackle the toil in your backlog. Our agents have all the same tools as a human developer: they can modify code, run commands, browse the web,
call APIs, and yes-even copy code snippets from StackOverflow.
<br/>
<Link to="https://docs.all-hands.dev/modules/usage/installation"
style={{
textDecoration: 'underline',
display: 'inline-block',
marginTop: '0.5rem'
}}
>
Get started with OpenHands.
</Link>
</p>
</div>
<div align="center" className="header-links">
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors" /></a>
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers" /></a>
@@ -50,9 +27,12 @@ export function HomepageHeader() {
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community" /></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits" /></a>
<br/>
<a href="https://docs.all-hands.dev/modules/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation" /></a>
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Paper on Arxiv" /></a>
<a href="https://huggingface.co/spaces/OpenHands/evaluation"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark Score" /></a>
</div>
<Demo />
</div>
</div>
);

View File

@@ -1,14 +1,14 @@
/* homepageHeader.css */
.homepage-header {
padding: 1rem 0;
height: 800px;
}
.header-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem;
padding: 2rem;
font-weight: 300;
width: 100%;
}
@@ -25,7 +25,6 @@
.header-subtitle {
font-size: 1.5rem;
margin: 0.5rem 0;
}
.header-links {

View File

@@ -2,7 +2,15 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import { HomepageHeader } from '../components/HomepageHeader/HomepageHeader';
import { translate } from '@docusaurus/Translate';
import { Demo } from "../components/Demo/Demo";
export function Header({ title, summary }): JSX.Element {
return (
<div>
<h1>{title}</h1>
<h2 style={{ fontSize: '3rem' }}>{summary}</h2>
</div>
);
}
export default function Home(): JSX.Element {
const { siteConfig } = useDocusaurusContext();
@@ -15,14 +23,11 @@ export default function Home(): JSX.Element {
})}
>
<HomepageHeader />
<div style={{ textAlign: 'center', padding: '1rem 0' }}>
<Demo />
</div>
<div style={{ textAlign: 'center', padding: '0.5rem 2rem 1.5rem' }}>
<div style={{ textAlign: 'center', padding: '2rem' }}>
<br />
<h2>Most Popular Links</h2>
<ul style={{ listStyleType: 'none'}}>
<li><a href="/modules/usage/Installation">How to Run OpenHands</a></li>
<li><a href="/modules/usage/prompting/microagents-repo">Customizing OpenHands to a repository</a></li>
<li><a href="/modules/usage/how-to/github-action">Integrating OpenHands with Github</a></li>
<li><a href="/modules/usage/llms#model-recommendations">Recommended models to use</a></li>

View File

@@ -24,7 +24,6 @@ from openhands.core.config import (
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import MessageAction
from openhands.utils.async_utils import call_async_from_sync
game = None
@@ -122,7 +121,6 @@ def process_instance(
# Here's how you can run the agent (similar to the `main` function) and get the final task state
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
state: State | None = asyncio.run(
run_controller(

View File

@@ -34,7 +34,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import AgentFinishAction, CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
def get_config(
@@ -211,7 +210,6 @@ def process_instance(
# =============================================
runtime: Runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance=instance)

View File

@@ -34,7 +34,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
# Configure visibility of unit tests to the Agent.
USE_UNIT_TESTS = os.environ.get('USE_UNIT_TESTS', 'false').lower() == 'true'
@@ -204,7 +203,7 @@ def process_instance(
# =============================================
runtime: Runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance=instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state

View File

@@ -31,7 +31,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': functools.partial(
@@ -275,7 +274,6 @@ def process_instance(
instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state

View File

@@ -34,7 +34,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
def codeact_user_response(state: State) -> str:
@@ -400,7 +399,6 @@ def process_instance(
instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state

View File

@@ -25,7 +25,6 @@ from openhands.core.config import (
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import MessageAction
from openhands.utils.async_utils import call_async_from_sync
# Only CodeActAgent can delegate to BrowsingAgent
SUPPORTED_AGENT_CLS = {'CodeActAgent'}
@@ -75,7 +74,6 @@ def process_instance(
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
state: State | None = asyncio.run(
run_controller(

View File

@@ -35,7 +35,6 @@ from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation, ErrorObservation
from openhands.events.serialization.event import event_to_dict
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.shutdown_listener import sleep_if_should_continue
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
@@ -395,7 +394,6 @@ def process_instance(
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
try:
initialize_runtime(runtime, instance)

View File

@@ -34,7 +34,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import AgentFinishAction, CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
EVALUATION_LLM = 'gpt-4-1106-preview'
@@ -282,7 +281,6 @@ def process_instance(
# Here's how you can run the agent (similar to the `main` function) and get the final task state
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance.data_files)
state: State | None = asyncio.run(

View File

@@ -31,7 +31,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import AgentFinishAction, CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
DATASET_CACHE_DIR = os.path.join(os.path.dirname(__file__), 'data')
@@ -149,7 +148,6 @@ def process_instance(
logger.info(f'Instruction:\n{instruction}', extra={'msg_type': 'OBSERVATION'})
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state

View File

@@ -26,7 +26,6 @@ from openhands.core.config import (
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import MessageAction
from openhands.utils.async_utils import call_async_from_sync
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -83,7 +82,6 @@ def process_instance(
# Here's how you can run the agent (similar to the `main` function) and get the final task state
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
state: State | None = asyncio.run(
run_controller(
config=config,

View File

@@ -49,7 +49,6 @@ from openhands.events.action import (
MessageAction,
)
from openhands.events.observation import Observation
from openhands.utils.async_utils import call_async_from_sync
ACTION_FORMAT = """
<<FINAL_ANSWER||
@@ -215,7 +214,6 @@ Ok now its time to start solving the question. Good luck!
"""
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
state: State | None = asyncio.run(
run_controller(
config=config,

View File

@@ -39,7 +39,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
IMPORT_HELPER = {
'python': [
@@ -233,7 +232,6 @@ def process_instance(
# Here's how you can run the agent (similar to the `main` function) and get the final task state
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
state: State | None = asyncio.run(
run_controller(

View File

@@ -31,7 +31,6 @@ from openhands.events.action import (
)
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -207,7 +206,6 @@ def process_instance(
instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state

View File

@@ -41,7 +41,6 @@ from openhands.runtime.browser.browser_env import (
BROWSER_EVAL_GET_GOAL_ACTION,
BROWSER_EVAL_GET_REWARDS_ACTION,
)
from openhands.utils.async_utils import call_async_from_sync
SUPPORTED_AGENT_CLS = {'BrowsingAgent', 'CodeActAgent'}
@@ -146,7 +145,6 @@ def process_instance(
logger.info(f'Starting evaluation for instance {env_id}.')
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
task_str, obs = initialize_runtime(runtime)
task_str += (

View File

@@ -35,7 +35,6 @@ from openhands.events.action import (
)
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
def codeact_user_response_mint(state: State, task: Task, task_config: dict[str, int]):
@@ -185,7 +184,6 @@ def process_instance(
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime)
state: State | None = asyncio.run(

View File

@@ -43,7 +43,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
config = load_app_config()
@@ -235,7 +234,6 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
# Run the agent

View File

@@ -29,7 +29,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -196,7 +195,6 @@ If the program uses some packages that are incompatible, please figure out alter
"""
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state

View File

@@ -288,7 +288,7 @@ def process_instance(
'model_patch': model_patch,
'instance_id': instance_id,
},
test_log_path=test_output_path,
log_path=test_output_path,
include_tests_status=True,
)
report = _report[instance_id]

View File

@@ -40,7 +40,6 @@ from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation, ErrorObservation
from openhands.events.serialization.event import event_to_dict
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.shutdown_listener import sleep_if_should_continue
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
@@ -465,7 +464,6 @@ def process_instance(
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
try:
initialize_runtime(runtime, instance)

View File

@@ -7,7 +7,7 @@ import os
import re
from dataclasses import dataclass
from enum import Enum, auto
from typing import Dict, List, Union
from typing import Dict, List, Optional, Union
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import BrowseInteractiveAction
@@ -133,7 +133,7 @@ def parse_content_to_elements(content: str) -> Dict[str, str]:
return elements
def find_matching_anchor(content: str, selector: str) -> str | None:
def find_matching_anchor(content: str, selector: str) -> Optional[str]:
"""Find the anchor ID that matches the given selector description"""
elements = parse_content_to_elements(content)

View File

@@ -28,7 +28,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import BrowserOutputObservation, CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
def get_config(
@@ -276,7 +275,7 @@ if __name__ == '__main__':
args.task_image_name, task_short_name, temp_dir, agent_llm_config, agent_config
)
runtime: Runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
init_task_env(runtime, args.server_hostname, env_llm_config)
dependencies = load_dependencies(runtime)

View File

@@ -27,7 +27,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -105,7 +104,6 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
logger.info(f'Instruction:\n{instruction}', extra={'msg_type': 'OBSERVATION'})
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime)
# Here's how you can run the agent (similar to the `main` function) and get the final task state

View File

@@ -37,7 +37,6 @@ from openhands.runtime.browser.browser_env import (
BROWSER_EVAL_GET_GOAL_ACTION,
BROWSER_EVAL_GET_REWARDS_ACTION,
)
from openhands.utils.async_utils import call_async_from_sync
SUPPORTED_AGENT_CLS = {'VisualBrowsingAgent'}
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
@@ -160,8 +159,6 @@ def process_instance(
logger.info(f'Starting evaluation for instance {env_id}.')
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
task_str, goal_image_urls = initialize_runtime(runtime)
initial_user_action = MessageAction(content=task_str, image_urls=goal_image_urls)
state: State | None = asyncio.run(

View File

@@ -36,7 +36,6 @@ from openhands.runtime.browser.browser_env import (
BROWSER_EVAL_GET_GOAL_ACTION,
BROWSER_EVAL_GET_REWARDS_ACTION,
)
from openhands.utils.async_utils import call_async_from_sync
SUPPORTED_AGENT_CLS = {'BrowsingAgent'}
@@ -145,7 +144,6 @@ def process_instance(
logger.info(f'Starting evaluation for instance {env_id}.')
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
task_str = initialize_runtime(runtime)
state: State | None = asyncio.run(

View File

@@ -30,7 +30,6 @@ from openhands.core.main import create_runtime, run_controller
from openhands.events.action import MessageAction
from openhands.events.serialization.event import event_to_dict
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
FAKE_RESPONSES = {
'CodeActAgent': fake_user_response,
@@ -109,7 +108,6 @@ def process_instance(
# create sandbox and run the agent
# =============================================
runtime: Runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
try:
test_class.initialize_runtime(runtime)

View File

@@ -0,0 +1,35 @@
import { screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { renderWithProviders } from "test-utils";
import { RuntimeSizeSelector } from "#/components/shared/modals/settings/runtime-size-selector";
const renderRuntimeSizeSelector = () =>
renderWithProviders(<RuntimeSizeSelector isDisabled={false} />);
describe("RuntimeSizeSelector", () => {
it("should show both runtime size options", () => {
renderRuntimeSizeSelector();
// The options are in the hidden select element
const select = screen.getByRole("combobox", { hidden: true });
expect(select).toHaveValue("1");
expect(select).toHaveDisplayValue("1x (2 core, 8G)");
expect(select.children).toHaveLength(3); // Empty option + 2 size options
});
it("should show the full description text for disabled options", async () => {
renderRuntimeSizeSelector();
// Click the button to open the dropdown
const button = screen.getByRole("button", {
name: "1x (2 core, 8G) SETTINGS_FORM$RUNTIME_SIZE_LABEL",
});
button.click();
// Wait for the dropdown to open and find the description text
const description = await screen.findByText(
"Runtime sizes over 1 are disabled by default, please contact contact@all-hands.dev to get access to larger runtimes.",
);
expect(description).toBeInTheDocument();
expect(description).toHaveClass("whitespace-normal", "break-words");
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -7,47 +7,47 @@
"node": ">=20.0.0"
},
"dependencies": {
"@heroui/react": "2.7.4",
"@heroui/react": "2.6.14",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.2.0",
"@react-router/serve": "^7.2.0",
"@react-router/node": "^7.1.5",
"@react-router/serve": "^7.1.5",
"@react-types/shared": "^3.27.0",
"@reduxjs/toolkit": "^2.6.0",
"@reduxjs/toolkit": "^2.5.1",
"@stripe/react-stripe-js": "^3.1.1",
"@stripe/stripe-js": "^5.7.0",
"@tanstack/react-query": "^5.66.11",
"@stripe/stripe-js": "^5.5.0",
"@tanstack/react-query": "^5.66.7",
"@vitejs/plugin-react": "^4.3.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.8.1",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.4.7",
"framer-motion": "^12.4.4",
"i18next": "^24.2.2",
"i18next-browser-languagedetector": "^8.0.4",
"i18next-browser-languagedetector": "^8.0.3",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.23",
"jose": "^6.0.8",
"isbot": "^5.1.22",
"jose": "^5.10.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.225.1",
"posthog-js": "^1.219.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.5.1",
"react-i18next": "^15.4.1",
"react-icons": "^5.5.0",
"react-markdown": "^10.0.1",
"react-icons": "^5.4.0",
"react-markdown": "^9.0.3",
"react-redux": "^9.2.0",
"react-router": "^7.2.0",
"react-router": "^7.1.5",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.7",
"remark-gfm": "^4.0.1",
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.0.2",
"vite": "^6.2.0",
"tailwind-merge": "^3.0.1",
"vite": "^6.1.0",
"web-vitals": "^3.5.2",
"ws": "^8.18.1"
"ws": "^8.18.0"
},
"scripts": {
"dev": "npm run make-i18n && cross-env VITE_MOCK_API=false react-router dev",
@@ -81,14 +81,14 @@
"devDependencies": {
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.50.1",
"@react-router/dev": "^7.2.0",
"@react-router/dev": "^7.1.5",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.66.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^22.13.8",
"@types/node": "^22.13.4",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@types/react-highlight": "^0.12.8",
@@ -96,13 +96,13 @@
"@types/ws": "^8.5.14",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^3.0.7",
"@vitest/coverage-v8": "^3.0.6",
"autoprefixer": "^10.4.20",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^10.0.2",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.3",
@@ -113,10 +113,10 @@
"lint-staged": "^15.4.3",
"msw": "^2.6.6",
"postcss": "^8.5.2",
"prettier": "^3.5.3",
"stripe": "^17.7.0",
"prettier": "^3.5.1",
"stripe": "^17.5.0",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.2",
"typescript": "^5.7.3",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.2"

View File

@@ -174,7 +174,7 @@ class OpenHands {
code: string,
): Promise<GitHubAccessTokenResponse> {
const { data } = await openHands.post<GitHubAccessTokenResponse>(
"/api/keycloak/callback",
"/api/github/callback",
{
code,
},

View File

@@ -14,10 +14,7 @@ import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
import { FeedbackModal } from "../feedback/feedback-modal";
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
import { TypingIndicator } from "./typing-indicator";
import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
import { useWsClient } from "#/context/ws-client-provider";
import { Messages } from "./messages";
import { ChatSuggestions } from "./chat-suggestions";
import { ActionSuggestions } from "./action-suggestions";
@@ -37,7 +34,7 @@ function getEntryPoint(
}
export function ChatInterface() {
const { send, isLoadingMessages, status, pendingMessages } = useWsClient();
const { send, isLoadingMessages } = useWsClient();
const dispatch = useDispatch();
const scrollRef = React.useRef<HTMLDivElement>(null);
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
@@ -57,9 +54,6 @@ export function ChatInterface() {
const params = useParams();
const { mutate: getTrajectory } = useGetTrajectory();
const isClientDisconnected = status === WsClientProviderStatus.DISCONNECTED;
const hasPendingMessages = pendingMessages.length > 0;
const handleSendMessage = async (content: string, files: File[]) => {
if (messages.length === 0) {
posthog.capture("initial_query_submitted", {
@@ -82,16 +76,7 @@ export function ChatInterface() {
const timestamp = new Date().toISOString();
const pending = true;
dispatch(addUserMessage({ content, imageUrls, timestamp, pending }));
// Create the chat message
const chatMessage = createChatMessage(content, imageUrls, timestamp);
// Send or queue the message depending on connection status
send(chatMessage);
// Set agent state to RUNNING when a message is sent
send(generateAgentStateChangeEvent(AgentState.RUNNING));
send(createChatMessage(content, imageUrls, timestamp));
setMessageToSend(null);
};
@@ -146,20 +131,8 @@ export function ChatInterface() {
className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2"
>
{isLoadingMessages && (
<div className="flex flex-col items-center gap-2">
<div className="flex justify-center">
<LoadingSpinner size="small" />
{isClientDisconnected && (
<div className="text-sm text-neutral-400">
Waiting for client to become ready...
{hasPendingMessages && (
<div className="text-xs text-neutral-500 mt-1">
{pendingMessages.length} message
{pendingMessages.length !== 1 ? "s" : ""} will be sent when
connected
</div>
)}
</div>
)}
</div>
)}
@@ -206,7 +179,7 @@ export function ChatInterface() {
onSubmit={handleSendMessage}
onStop={handleStop}
isDisabled={
// Allow input even when loading, but not during confirmation
curAgentState === AgentState.LOADING ||
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
}
mode={curAgentState === AgentState.RUNNING ? "stop" : "submit"}

View File

@@ -57,19 +57,18 @@ export function ChatMessage({
onClick={handleCopyToClipboard}
mode={isCopy ? "copied" : "copy"}
/>
<div className="text-sm overflow-auto break-words">
<Markdown
components={{
code,
ul,
ol,
a: anchor,
}}
remarkPlugins={[remarkGfm]}
>
{message}
</Markdown>
</div>
<Markdown
className="text-sm overflow-auto break-words"
components={{
code,
ul,
ol,
a: anchor,
}}
remarkPlugins={[remarkGfm]}
>
{message}
</Markdown>
{children}
</article>
);

View File

@@ -123,18 +123,17 @@ export function ExpandableMessage({
)}
</div>
{(!headline || showDetails) && (
<div className="text-sm overflow-auto">
<Markdown
components={{
code,
ul,
ol,
}}
remarkPlugins={[remarkGfm]}
>
{details}
</Markdown>
</div>
<Markdown
className="text-sm overflow-auto"
components={{
code,
ul,
ol,
}}
remarkPlugins={[remarkGfm]}
>
{details}
</Markdown>
)}
</div>
</div>

View File

@@ -22,11 +22,10 @@ export function AgentStatusBar() {
const { t, i18n } = useTranslation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curStatusMessage } = useSelector((state: RootState) => state.status);
const { status, pendingMessages } = useWsClient();
const { status } = useWsClient();
const { notify } = useNotification();
const [statusMessage, setStatusMessage] = React.useState<string>("");
const hasPendingMessages = pendingMessages.length > 0;
const updateStatusMessage = () => {
let message = curStatusMessage.message || "";
@@ -72,13 +71,7 @@ export function AgentStatusBar() {
React.useEffect(() => {
if (status === WsClientProviderStatus.DISCONNECTED) {
if (hasPendingMessages) {
setStatusMessage(
`Connecting... (${pendingMessages.length} pending message${pendingMessages.length !== 1 ? "s" : ""})`,
);
} else {
setStatusMessage("Connecting...");
}
setStatusMessage("Connecting...");
} else {
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
if (notificationStates.includes(curAgentState)) {
@@ -94,7 +87,7 @@ export function AgentStatusBar() {
}
}
}
}, [curAgentState, status, pendingMessages.length, notify, t]);
}, [curAgentState, notify, t]);
return (
<div className="flex flex-col items-center">

View File

@@ -99,6 +99,7 @@ export function GitHubRepositorySelector({
<AutocompleteItem
data-testid="github-repo-item"
key={repo.id}
value={repo.id}
className="data-[selected=true]:bg-default-100"
textValue={repo.full_name}
>
@@ -113,6 +114,7 @@ export function GitHubRepositorySelector({
<AutocompleteItem
data-testid="github-repo-item"
key={repo.id}
value={repo.id}
className="data-[selected=true]:bg-default-100"
textValue={repo.full_name}
>

View File

@@ -0,0 +1,43 @@
import { Autocomplete, AutocompleteItem } from "@heroui/react";
interface FormFieldsetProps {
id: string;
label: string;
items: { key: string; value: string }[];
defaultSelectedKey?: string;
isClearable?: boolean;
}
export function FormFieldset({
id,
label,
items,
defaultSelectedKey,
isClearable,
}: FormFieldsetProps) {
return (
<fieldset className="flex flex-col gap-2">
<label htmlFor={id} className="font-[500] text-[#A3A3A3] text-xs">
{label}
</label>
<Autocomplete
id={id}
name={id}
aria-label={label}
defaultSelectedKey={defaultSelectedKey}
isClearable={isClearable}
inputProps={{
classNames: {
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
},
}}
>
{items.map((item) => (
<AutocompleteItem key={item.key} value={item.key}>
{item.value}
</AutocompleteItem>
))}
</Autocomplete>
</fieldset>
);
}

View File

@@ -0,0 +1,46 @@
import { Autocomplete, AutocompleteItem } from "@heroui/react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface AgentInputProps {
isDisabled: boolean;
defaultValue: string;
agents: string[];
}
export function AgentInput({
isDisabled,
defaultValue,
agents,
}: AgentInputProps) {
const { t } = useTranslation();
return (
<fieldset data-testid="agent-selector" className="flex flex-col gap-2">
<label htmlFor="agent" className="font-[500] text-[#A3A3A3] text-xs">
{t(I18nKey.SETTINGS_FORM$AGENT_LABEL)}
</label>
<Autocomplete
isDisabled={isDisabled}
isRequired
id="agent"
aria-label="Agent"
data-testid="agent-input"
name="agent"
defaultSelectedKey={defaultValue}
isClearable={false}
inputProps={{
classNames: {
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
},
}}
>
{agents.map((agent) => (
<AutocompleteItem key={agent} value={agent}>
{agent}
</AutocompleteItem>
))}
</Autocomplete>
</fieldset>
);
}

View File

@@ -0,0 +1,46 @@
import { Autocomplete, AutocompleteItem } from "@heroui/react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface SecurityAnalyzerInputProps {
isDisabled: boolean;
defaultValue: string;
securityAnalyzers: string[];
}
export function SecurityAnalyzerInput({
isDisabled,
defaultValue,
securityAnalyzers,
}: SecurityAnalyzerInputProps) {
const { t } = useTranslation();
return (
<fieldset className="flex flex-col gap-2">
<label
htmlFor="security-analyzer"
className="font-[500] text-[#A3A3A3] text-xs"
>
{t(I18nKey.SETTINGS_FORM$SECURITY_ANALYZER_LABEL)}
</label>
<Autocomplete
isDisabled={isDisabled}
id="security-analyzer"
name="security-analyzer"
aria-label="Security Analyzer"
defaultSelectedKey={defaultValue}
inputProps={{
classNames: {
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
},
}}
>
{securityAnalyzers.map((analyzer) => (
<AutocompleteItem key={analyzer} value={analyzer}>
{analyzer}
</AutocompleteItem>
))}
</Autocomplete>
</fieldset>
);
}

View File

@@ -100,6 +100,7 @@ export function ModelSelector({
<AutocompleteItem
data-testid={`provider-item-${provider}`}
key={provider}
value={provider}
>
{mapProvider(provider)}
</AutocompleteItem>
@@ -109,7 +110,7 @@ export function ModelSelector({
{Object.keys(models)
.filter((provider) => !VERIFIED_PROVIDERS.includes(provider))
.map((provider) => (
<AutocompleteItem key={provider}>
<AutocompleteItem key={provider} value={provider}>
{mapProvider(provider)}
</AutocompleteItem>
))}
@@ -147,7 +148,9 @@ export function ModelSelector({
{models[selectedProvider || ""]?.models
.filter((model) => VERIFIED_MODELS.includes(model))
.map((model) => (
<AutocompleteItem key={model}>{model}</AutocompleteItem>
<AutocompleteItem key={model} value={model}>
{model}
</AutocompleteItem>
))}
</AutocompleteSection>
<AutocompleteSection title="Others">
@@ -157,6 +160,7 @@ export function ModelSelector({
<AutocompleteItem
data-testid={`model-item-${model}`}
key={model}
value={model}
>
{model}
</AutocompleteItem>

View File

@@ -0,0 +1,57 @@
import { useTranslation } from "react-i18next";
import { Select, SelectItem } from "@heroui/react";
import { I18nKey } from "#/i18n/declaration";
interface RuntimeSizeSelectorProps {
isDisabled: boolean;
defaultValue?: number;
}
export function RuntimeSizeSelector({
isDisabled,
defaultValue,
}: RuntimeSizeSelectorProps) {
const { t } = useTranslation();
return (
<fieldset className="flex flex-col gap-2">
<label
htmlFor="runtime-size"
className="font-[500] text-[#A3A3A3] text-xs"
>
{t(I18nKey.SETTINGS_FORM$RUNTIME_SIZE_LABEL)}
</label>
<Select
data-testid="runtime-size"
id="runtime-size"
name="runtime-size"
defaultSelectedKeys={[String(defaultValue || 1)]}
selectedKeys={[String(defaultValue || 1)]}
isDisabled={isDisabled}
selectionMode="single"
disallowEmptySelection
aria-label={t(I18nKey.SETTINGS_FORM$RUNTIME_SIZE_LABEL)}
classNames={{
trigger: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
}}
>
<SelectItem key="1" value={1}>
1x (2 core, 8G)
</SelectItem>
<SelectItem
key="2"
value={2}
isDisabled
classNames={{
description:
"whitespace-normal break-words min-w-[300px] max-w-[300px]",
base: "min-w-[300px] max-w-[300px]",
}}
description="Runtime sizes over 1 are disabled by default, please contact contact@all-hands.dev to get access to larger runtimes."
>
2x (4 core, 16G)
</SelectItem>
</Select>
</fieldset>
);
}

View File

@@ -39,14 +39,6 @@ const isMessageAction = (
): event is UserMessageAction | AssistantMessageAction =>
isUserMessage(event) || isAssistantMessage(event);
// Check if an event is an agent state changed observation
const isAgentStateEvent = (event: Record<string, unknown>): boolean =>
isOpenHandsEvent(event) &&
"type" in event &&
event.type === "observation" &&
"observation_id" in event &&
event.observation_id === "agent_state_changed";
export enum WsClientProviderStatus {
CONNECTED,
DISCONNECTED,
@@ -57,21 +49,15 @@ interface UseWsClient {
isLoadingMessages: boolean;
events: Record<string, unknown>[];
send: (event: Record<string, unknown>) => void;
queueMessage: (event: Record<string, unknown>) => void;
pendingMessages: Record<string, unknown>[];
}
const WsClientContext = React.createContext<UseWsClient>({
status: WsClientProviderStatus.DISCONNECTED,
isLoadingMessages: true,
events: [],
pendingMessages: [],
send: () => {
throw new Error("not connected");
},
queueMessage: () => {
throw new Error("not connected");
},
});
interface WsClientProviderProps {
@@ -123,50 +109,26 @@ export function WsClientProvider({
WsClientProviderStatus.DISCONNECTED,
);
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
const [pendingMessages, setPendingMessages] = React.useState<
Record<string, unknown>[]
>([]);
const [backendReady, setBackendReady] = React.useState(false);
const lastEventRef = React.useRef<Record<string, unknown> | null>(null);
const messageRateHandler = useRate({ threshold: 250 });
function queueMessage(event: Record<string, unknown>) {
setPendingMessages((prev) => [...prev, event]);
}
function send(event: Record<string, unknown>) {
if (!sioRef.current) {
EventLogger.error("WebSocket is not connected.");
queueMessage(event);
return;
}
if (!backendReady) {
// If backend is not ready yet, queue the message
EventLogger.info("Backend not ready, queueing message");
queueMessage(event);
return;
}
sioRef.current.emit("oh_action", event);
}
function handleConnect() {
setStatus(WsClientProviderStatus.CONNECTED);
// Don't send queued messages yet - wait for backend ready signal
}
function handleMessage(event: Record<string, unknown>) {
if (isOpenHandsEvent(event) && isMessageAction(event)) {
messageRateHandler.record(new Date().getTime());
}
// Check if this is a state change event indicating backend is ready
if (isAgentStateEvent(event)) {
setBackendReady(true);
}
setEvents((prevEvents) => [...prevEvents, event]);
if (!Number.isNaN(parseInt(event.id as string, 10))) {
lastEventRef.current = event;
@@ -177,7 +139,6 @@ export function WsClientProvider({
function handleDisconnect(data: unknown) {
setStatus(WsClientProviderStatus.DISCONNECTED);
setBackendReady(false);
const sio = sioRef.current;
if (!sio) {
return;
@@ -189,34 +150,11 @@ export function WsClientProvider({
function handleError(data: unknown) {
setStatus(WsClientProviderStatus.DISCONNECTED);
setBackendReady(false);
updateStatusWhenErrorMessagePresent(data);
}
// Watch for backend ready state and send queued messages when ready
React.useEffect(() => {
if (backendReady && pendingMessages.length > 0 && sioRef.current) {
// Backend is ready and we have pending messages
EventLogger.info(`Sending ${pendingMessages.length} queued messages`);
pendingMessages.forEach((event) => {
sioRef.current?.emit("oh_action", event);
});
// Also set the agent state to RUNNING if needed
const agentStateEvent = {
action: "change_agent_state",
args: { agent_state: "running" },
};
sioRef.current.emit("oh_action", agentStateEvent);
setPendingMessages([]);
}
}, [backendReady, pendingMessages.length]);
React.useEffect(() => {
lastEventRef.current = null;
setBackendReady(false);
}, [conversationId]);
React.useEffect(() => {
@@ -272,11 +210,9 @@ export function WsClientProvider({
status,
isLoadingMessages: messageRateHandler.isUnderThreshold,
events,
pendingMessages,
send,
queueMessage,
}),
[status, messageRateHandler.isUnderThreshold, events, pendingMessages],
[status, messageRateHandler.isUnderThreshold, events],
);
return <WsClientContext value={value}>{children}</WsClientContext>;

View File

@@ -257,7 +257,6 @@ export enum I18nKey {
STATUS$ERROR_LLM_AUTHENTICATION = "STATUS$ERROR_LLM_AUTHENTICATION",
STATUS$ERROR_LLM_SERVICE_UNAVAILABLE = "STATUS$ERROR_LLM_SERVICE_UNAVAILABLE",
STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR = "STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR",
STATUS$ERROR_LLM_OUT_OF_CREDITS = "STATUS$ERROR_LLM_OUT_OF_CREDITS",
STATUS$ERROR_RUNTIME_DISCONNECTED = "STATUS$ERROR_RUNTIME_DISCONNECTED",
STATUS$LLM_RETRY = "STATUS$LLM_RETRY",
AGENT_ERROR$BAD_ACTION = "AGENT_ERROR$BAD_ACTION",
@@ -280,7 +279,6 @@ export enum I18nKey {
ACTION_MESSAGE$EDIT = "ACTION_MESSAGE$EDIT",
ACTION_MESSAGE$WRITE = "ACTION_MESSAGE$WRITE",
ACTION_MESSAGE$BROWSE = "ACTION_MESSAGE$BROWSE",
ACTION_MESSAGE$THINK = "ACTION_MESSAGE$THINK",
OBSERVATION_MESSAGE$RUN = "OBSERVATION_MESSAGE$RUN",
OBSERVATION_MESSAGE$RUN_IPYTHON = "OBSERVATION_MESSAGE$RUN_IPYTHON",
OBSERVATION_MESSAGE$READ = "OBSERVATION_MESSAGE$READ",
@@ -308,8 +306,11 @@ export enum I18nKey {
STATUS$WAITING_FOR_CLIENT = "STATUS$WAITING_FOR_CLIENT",
SUGGESTIONS$WHAT_TO_BUILD = "SUGGESTIONS$WHAT_TO_BUILD",
SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL = "SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL",
BUTTON$ENABLE_SOUND = "BUTTON$ENABLE_SOUND",
BUTTON$DISABLE_SOUND = "BUTTON$DISABLE_SOUND",
BUTTON$MARK_HELPFUL = "BUTTON$MARK_HELPFUL",
BUTTON$MARK_NOT_HELPFUL = "BUTTON$MARK_NOT_HELPFUL",
NOTIFICATION$SOUND_ENABLED = "NOTIFICATION$SOUND_ENABLED",
NOTIFICATION$SOUND_DISABLED = "NOTIFICATION$SOUND_DISABLED",
BUTTON$EXPORT_CONVERSATION = "BUTTON$EXPORT_CONVERSATION",
BILLING$CLICK_TO_TOP_UP = "BILLING$CLICK_TO_TOP_UP",
}

View File

@@ -57,28 +57,6 @@ const messageActions = {
store.dispatch(appendJupyterInput(message.args.code));
}
},
[ActionType.FINISH]: (message: ActionMessage) => {
store.dispatch(addAssistantMessage(message.args.final_thought));
let successPrediction = "";
if (message.args.task_completed === "partial") {
successPrediction =
"The agent thinks that the task was **completed partially**.";
} else if (message.args.task_completed === "false") {
successPrediction =
"The agent thinks that the task was **not completed**.";
} else if (message.args.task_completed === "true") {
successPrediction =
"The agent thinks that the task was **completed successfully**.";
}
if (successPrediction) {
// if final_thought is not empty, add a new line before the success prediction
if (message.args.final_thought) {
store.dispatch(addAssistantMessage(`\n${successPrediction}`));
} else {
store.dispatch(addAssistantMessage(successPrediction));
}
}
},
};
export function handleActionMessage(message: ActionMessage) {

View File

@@ -89,16 +89,6 @@ export function handleObservationMessage(message: ObservationMessage) {
);
break;
case "read":
store.dispatch(
addAssistantObservation({
...baseObservation,
observation,
extras: {
path: String(message.extras.path || ""),
},
}),
);
break;
case "edit":
store.dispatch(
addAssistantObservation({
@@ -106,7 +96,6 @@ export function handleObservationMessage(message: ObservationMessage) {
observation,
extras: {
path: String(message.extras.path || ""),
diff: String(message.extras.diff || ""),
},
}),
);

View File

@@ -173,14 +173,9 @@ export const chatSlice = createSlice({
causeMessage.content
}\n\nOutput:\n\`\`\`\n${content.trim() || "[Command finished execution with no output]"}\n\`\`\``;
causeMessage.content = content; // Observation content includes the action
} else if (observationID === "read") {
causeMessage.content = `\`\`\`\n${observation.payload.content}\n\`\`\``; // Content is already truncated by the ACI
} else if (observationID === "edit") {
if (causeMessage.success) {
causeMessage.content = `\`\`\`diff\n${observation.payload.extras.diff}\n\`\`\``; // Content is already truncated by the ACI
} else {
causeMessage.content = observation.payload.content;
}
} else if (observationID === "read" || observationID === "edit") {
const { content } = observation.payload;
causeMessage.content = `\`\`\`${observationID === "edit" ? "diff" : "python"}\n${content}\n\`\`\``; // Content is already truncated by the ACI
} else if (observationID === "browse") {
let content = `**URL:** ${observation.payload.extras.url}\n`;
if (observation.payload.extras.error) {

View File

@@ -51,8 +51,6 @@ export interface ThinkAction extends OpenHandsActionEvent<"think"> {
export interface FinishAction extends OpenHandsActionEvent<"finish"> {
source: "agent";
args: {
final_thought: string;
task_completed: "success" | "failure" | "partial";
outputs: Record<string, unknown>;
thought: string;
};

View File

@@ -70,7 +70,6 @@ export interface EditObservation extends OpenHandsObservationEvent<"edit"> {
source: "agent";
extras: {
path: string;
diff: string;
};
}

View File

@@ -37,16 +37,6 @@ class EventLogger {
}
}
/**
* Log an info message
* @param info The info message
*/
static info(info: string) {
if (this.isDevMode) {
console.info(info);
}
}
/**
* Log an error message
* @param error The error message

View File

@@ -5,11 +5,7 @@
* @returns The URL to redirect to for GitHub OAuth
*/
export const generateGitHubAuthUrl = (clientId: string, requestUrl: URL) => {
const redirectUri = `${requestUrl.origin}/oauth/keycloak/callback`;
const authUrl = requestUrl.hostname
.replace(/(^|\.)staging\.all-hands\.dev$/, "$1auth.staging.all-hands.dev")
.replace(/(^|\.)app\.all-hands\.dev$/, "auth.app.all-hands.dev")
.replace(/(^|\.)localhost$/, "auth.staging.all-hands.dev");
const scope = "openid email profile offline_access";
return `https://${authUrl}/realms/allhands/protocol/openid-connect/auth?client_id=github&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
const redirectUri = `${requestUrl.origin}/oauth/github/callback`;
const scope = "repo,user,workflow,offline_access";
return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
};

View File

@@ -1,55 +0,0 @@
---
name: docker
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- docker
- container
---
# Docker Installation and Usage Guide
## Installation on Debian/Ubuntu Systems
To install Docker on a Debian/Ubuntu system, follow these steps:
```bash
# Update package index
sudo apt-get update
# Install prerequisites
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Set up the stable repository
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Update package index again
sudo apt-get update
# Install Docker Engine
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
```
## Starting Docker in Container Environments
If you're in a container environment without systemd (like this workspace), start Docker with:
```bash
# Start Docker daemon in the background
sudo dockerd > /tmp/docker.log 2>&1 &
# Wait for Docker to initialize
sleep 5
```
## Verifying Docker Installation
To verify Docker is working correctly, run the hello-world container:
```bash
sudo docker run hello-world
```

View File

@@ -1,50 +0,0 @@
---
name: kubernetes
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- kubernetes
- k8s
- kube
---
# Kubernetes Local Development with KIND
## KIND Installation and Setup
KIND (Kubernetes IN Docker) is a tool for running local Kubernetes clusters using Docker containers as nodes. It's designed for testing Kubernetes applications locally.
IMPORTANT: Before you proceed with installation, make sure you have docker installed locally.
### Installation
To install KIND on a Debian/Ubuntu system:
```bash
# Download KIND binary
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
# Make it executable
chmod +x ./kind
# Move to a directory in your PATH
sudo mv ./kind /usr/local/bin/
```
To install kubectl:
```bash
# Download kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
# Make it executable
chmod +x kubectl
# Move to a directory in your PATH
sudo mv ./kubectl /usr/local/bin/
```
### Creating a Cluster
Create a basic KIND cluster:
```bash
kind create cluster
```

View File

@@ -2,7 +2,6 @@ import json
import os
from collections import deque
import openhands
import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
@@ -77,21 +76,14 @@ class CodeActAgent(Agent):
f'TOOLS loaded for CodeActAgent: {json.dumps(self.tools, indent=2, ensure_ascii=False).replace("\\n", "\n")}'
)
self.prompt_manager = PromptManager(
microagent_dir=os.path.join(
os.path.dirname(os.path.dirname(openhands.__file__)),
'microagents',
)
if self.config.enable_prompt_extensions
else None,
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
disabled_microagents=self.config.disabled_microagents,
)
# Create a ConversationMemory instance
self.conversation_memory = ConversationMemory(self.prompt_manager)
self.condenser = Condenser.from_config(self.config.condenser)
logger.debug(f'Using condenser: {type(self.condenser)}')
logger.debug(f'Using condenser: {self.condenser}')
def reset(self) -> None:
"""Resets the CodeAct Agent."""
@@ -222,8 +214,6 @@ class CodeActAgent(Agent):
# enhance the user message with additional context based on keywords matched
if msg.role == 'user':
self.prompt_manager.enhance_message(msg)
# Add double newline between consecutive user messages
if prev_role == 'user' and len(msg.content) > 0:
# Find the first TextContent in the message to add newlines
@@ -232,7 +222,6 @@ class CodeActAgent(Agent):
# If the previous message was also from a user, prepend two newlines to ensure separation
content_item.text = '\n\n' + content_item.text
break
results.append(msg)
prev_role = msg.role

View File

@@ -108,10 +108,7 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
# AgentFinishAction
# ================================================
elif tool_call.function.name == FinishTool['function']['name']:
action = AgentFinishAction(
final_thought=arguments.get('message', ''),
task_completed=arguments.get('task_completed', None),
)
action = AgentFinishAction()
# ================================================
# LLMBasedFileEditTool (LLM-based file editor, deprecated)

View File

@@ -1,8 +0,0 @@
{% for agent_info in triggered_agents %}
<EXTRA_INFO>
The following information has been included based on a keyword match for "{{ agent_info.trigger_word }}".
It may or may not be relevant to the user's request.
{{ agent_info.agent.content }}
</EXTRA_INFO>
{% endfor %}

View File

@@ -1,39 +1,11 @@
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
_FINISH_DESCRIPTION = """Signals the completion of the current task or conversation.
Use this tool when:
- You have successfully completed the user's requested task
- You cannot proceed further due to technical limitations or missing information
The message should include:
- A clear summary of actions taken and their results
- Any next steps for the user
- Explanation if you're unable to complete the task
- Any follow-up questions if more information is needed
The task_completed field should be set to True if you believed you have completed the task, and False otherwise.
"""
_FINISH_DESCRIPTION = """Finish the interaction when the task is complete OR if the assistant cannot proceed further with the task."""
FinishTool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='finish',
description=_FINISH_DESCRIPTION,
parameters={
'type': 'object',
'required': ['message', 'task_completed'],
'properties': {
'message': {
'type': 'string',
'description': 'Final message to send to the user',
},
'task_completed': {
'type': 'string',
'enum': ['true', 'false', 'partial'],
'description': 'Whether you have completed the task.',
},
},
},
),
)

View File

@@ -248,9 +248,8 @@ class AgentController:
)
reported = RuntimeError(
'There was an unexpected error while running the agent. Please '
'report this error to the developers by opening an issue at '
'https://github.com/All-Hands-AI/OpenHands. Your session ID is '
f' {self.id}. Error type: {e.__class__.__name__}'
f'report this error to the developers. Your session ID is {self.id}. '
f'Error type: {e.__class__.__name__}'
)
if (
isinstance(e, litellm.AuthenticationError)
@@ -698,13 +697,10 @@ class AgentController:
except (ContextWindowExceededError, BadRequestError, OpenAIError) as e:
# FIXME: this is a hack until a litellm fix is confirmed
# Check if this is a nested context window error
# We have to rely on string-matching because LiteLLM doesn't consistently
# wrap the failure in a ContextWindowExceededError
error_str = str(e).lower()
if (
'contextwindowexceedederror' in error_str
or 'prompt is too long' in error_str
or 'input length and `max_tokens` exceed context limit' in error_str
or isinstance(e, ContextWindowExceededError)
):
if self.agent.config.enable_history_truncation:

View File

@@ -14,12 +14,7 @@ from openhands.core.config import (
from openhands.core.logger import openhands_logger as logger
from openhands.core.loop import run_agent_until_done
from openhands.core.schema import AgentState
from openhands.core.setup import (
create_agent,
create_controller,
create_runtime,
initialize_repository_for_runtime,
)
from openhands.core.setup import create_agent, create_controller, create_runtime
from openhands.events import EventSource, EventStreamSubscriber
from openhands.events.action import (
Action,
@@ -114,6 +109,7 @@ async def main(loop: asyncio.AbstractEventLoop):
sid=sid,
headless_mode=True,
agent=agent,
selected_repository=config.sandbox.selected_repo,
)
controller, _ = create_controller(agent, runtime, config)
@@ -169,14 +165,6 @@ async def main(loop: asyncio.AbstractEventLoop):
await runtime.connect()
# Initialize repository if needed
if config.sandbox.selected_repo:
initialize_repository_for_runtime(
runtime,
agent=agent,
selected_repository=config.sandbox.selected_repo,
)
if initial_user_action:
# If there's an initial user action, enqueue it and do not prompt again
event_stream.add_event(initial_user_action, EventSource.USER)

View File

@@ -46,6 +46,7 @@ class AppConfig(BaseModel):
file_uploads_allowed_extensions: Allowed file extensions. `['.*']` allows all.
cli_multiline_input: Whether to enable multiline input in CLI. When disabled,
input is read line by line. When enabled, input continues until /exit command.
microagents_dir: Directory containing global microagents.
"""
llms: dict[str, LLMConfig] = Field(default_factory=dict)
@@ -82,7 +83,10 @@ class AppConfig(BaseModel):
daytona_target: str = Field(default='us')
cli_multiline_input: bool = Field(default=False)
conversation_max_age_seconds: int = Field(default=864000) # 10 days in seconds
enable_default_condenser: bool = Field(default=True)
microagents_dir: str = Field(
default='microagents',
description='Directory containing global microagents',
)
defaults_dict: ClassVar[dict] = {}

View File

@@ -1,8 +1,7 @@
from typing import Literal, cast
from typing import Literal
from pydantic import BaseModel, Field, ValidationError
from pydantic import BaseModel, Field
from openhands.core import logger
from openhands.core.config.llm_config import LLMConfig
@@ -11,21 +10,17 @@ class NoOpCondenserConfig(BaseModel):
type: Literal['noop'] = Field('noop')
model_config = {'extra': 'forbid'}
class ObservationMaskingCondenserConfig(BaseModel):
"""Configuration for ObservationMaskingCondenser."""
type: Literal['observation_masking'] = Field('observation_masking')
attention_window: int = Field(
default=100,
default=10,
description='The number of most-recent events where observations will not be masked.',
ge=1,
)
model_config = {'extra': 'forbid'}
class RecentEventsCondenserConfig(BaseModel):
"""Configuration for RecentEventsCondenser."""
@@ -39,11 +34,9 @@ class RecentEventsCondenserConfig(BaseModel):
ge=0,
)
max_events: int = Field(
default=100, description='Maximum number of events to keep.', ge=1
default=10, description='Maximum number of events to keep.', ge=1
)
model_config = {'extra': 'forbid'}
class LLMSummarizingCondenserConfig(BaseModel):
"""Configuration for LLMCondenser."""
@@ -56,17 +49,13 @@ class LLMSummarizingCondenserConfig(BaseModel):
# at least one event by default, because the best guess is that it's the user task
keep_first: int = Field(
default=1,
description='Number of initial events to always keep in history.',
description='The number of initial events to condense.',
ge=0,
)
max_size: int = Field(
default=100,
description='Maximum size of the condensed history before triggering forgetting.',
ge=2,
default=10, description='Maximum number of events to keep.', ge=1
)
model_config = {'extra': 'forbid'}
class AmortizedForgettingCondenserConfig(BaseModel):
"""Configuration for AmortizedForgettingCondenser."""
@@ -85,8 +74,6 @@ class AmortizedForgettingCondenserConfig(BaseModel):
ge=0,
)
model_config = {'extra': 'forbid'}
class LLMAttentionCondenserConfig(BaseModel):
"""Configuration for LLMAttentionCondenser."""
@@ -108,10 +95,7 @@ class LLMAttentionCondenserConfig(BaseModel):
ge=0,
)
model_config = {'extra': 'forbid'}
# Type alias for convenience
CondenserConfig = (
NoOpCondenserConfig
| ObservationMaskingCondenserConfig
@@ -120,121 +104,3 @@ CondenserConfig = (
| AmortizedForgettingCondenserConfig
| LLMAttentionCondenserConfig
)
def condenser_config_from_toml_section(
data: dict, llm_configs: dict | None = None
) -> dict[str, CondenserConfig]:
"""
Create a CondenserConfig instance from a toml dictionary representing the [condenser] section.
For CondenserConfig, the handling is different since it's a union type. The type of condenser
is determined by the 'type' field in the section.
Example:
Parse condenser config like:
[condenser]
type = "noop"
For condensers that require an LLM config, you can specify the name of an LLM config:
[condenser]
type = "llm"
llm_config = "my_llm" # References [llm.my_llm] section
Args:
data: The TOML dictionary representing the [condenser] section.
llm_configs: Optional dictionary of LLMConfig objects keyed by name.
Returns:
dict[str, CondenserConfig]: A mapping where the key "condenser" corresponds to the configuration.
"""
# Initialize the result mapping
condenser_mapping: dict[str, CondenserConfig] = {}
# Process config
try:
# Determine which condenser type to use based on 'type' field
condenser_type = data.get('type', 'noop')
# Handle LLM config reference if needed
if (
condenser_type in ('llm', 'llm_attention')
and 'llm_config' in data
and isinstance(data['llm_config'], str)
):
llm_config_name = data['llm_config']
if llm_configs and llm_config_name in llm_configs:
# Replace the string reference with the actual LLMConfig object
data_copy = data.copy()
data_copy['llm_config'] = llm_configs[llm_config_name]
config = create_condenser_config(condenser_type, data_copy)
else:
logger.openhands_logger.warning(
f"LLM config '{llm_config_name}' not found for condenser. Using default LLMConfig."
)
# Create a default LLMConfig if the referenced one doesn't exist
data_copy = data.copy()
# Try to use the fallback 'llm' config
if llm_configs is not None:
data_copy['llm_config'] = llm_configs.get('llm')
config = create_condenser_config(condenser_type, data_copy)
else:
config = create_condenser_config(condenser_type, data)
condenser_mapping['condenser'] = config
except (ValidationError, ValueError) as e:
logger.openhands_logger.warning(
f'Invalid condenser configuration: {e}. Using NoOpCondenserConfig.'
)
# Default to NoOpCondenserConfig if config fails
config = NoOpCondenserConfig()
condenser_mapping['condenser'] = config
return condenser_mapping
# For backward compatibility
from_toml_section = condenser_config_from_toml_section
def create_condenser_config(condenser_type: str, data: dict) -> CondenserConfig:
"""
Create a CondenserConfig instance based on the specified type.
Args:
condenser_type: The type of condenser to create.
data: The configuration data.
Returns:
A CondenserConfig instance.
Raises:
ValueError: If the condenser type is unknown.
ValidationError: If the provided data fails validation for the condenser type.
"""
# Mapping of condenser types to their config classes
condenser_classes = {
'noop': NoOpCondenserConfig,
'observation_masking': ObservationMaskingCondenserConfig,
'recent': RecentEventsCondenserConfig,
'llm': LLMSummarizingCondenserConfig,
'amortized': AmortizedForgettingCondenserConfig,
'llm_attention': LLMAttentionCondenserConfig,
}
if condenser_type not in condenser_classes:
raise ValueError(f'Unknown condenser type: {condenser_type}')
# Create and validate the config using direct instantiation
# Explicitly handle ValidationError to provide more context
try:
config_class = condenser_classes[condenser_type]
# Use type casting to help mypy understand the return type
return cast(CondenserConfig, config_class(**data))
except ValidationError as e:
# Just re-raise with a more descriptive message, but don't try to pass the errors
# which can cause compatibility issues with different pydantic versions
raise ValueError(
f"Validation failed for condenser type '{condenser_type}': {e}"
)

View File

@@ -48,7 +48,7 @@ class LLMConfig(BaseModel):
reasoning_effort: The effort to put into reasoning. This is a string that can be one of 'low', 'medium', 'high', or 'none'. Exclusive for o1 models.
"""
model: str = Field(default='claude-3-7-sonnet-20250219')
model: str = Field(default='claude-3-5-sonnet-20241022')
api_key: SecretStr | None = Field(default=None)
base_url: str | None = Field(default=None)
api_version: str | None = Field(default=None)

View File

@@ -17,7 +17,6 @@ class SandboxConfig(BaseModel):
remote_runtime_api_timeout: The timeout for the remote runtime API requests.
enable_auto_lint: Whether to enable auto-lint.
use_host_network: Whether to use the host network.
runtime_binding_address: The binding address for the runtime ports. It specifies which network interface on the host machine Docker should bind the runtime ports to.
initialize_plugins: Whether to initialize plugins.
force_rebuild_runtime: Whether to force rebuild the runtime image.
runtime_extra_deps: The extra dependencies to install in the runtime image (typically used for evaluation).
@@ -61,7 +60,6 @@ class SandboxConfig(BaseModel):
default=False
) # once enabled, OpenHands would lint files after editing
use_host_network: bool = Field(default=False)
runtime_binding_address: str = Field(default='127.0.0.1')
runtime_extra_build_args: list[str] | None = Field(default=None)
initialize_plugins: bool = Field(default=True)
force_rebuild_runtime: bool = Field(default=False)
@@ -72,7 +70,7 @@ class SandboxConfig(BaseModel):
close_delay: int = Field(default=15)
remote_runtime_resource_factor: int = Field(default=1)
enable_gpu: bool = Field(default=False)
docker_runtime_kwargs: dict | None = Field(default=None)
docker_runtime_kwargs: str | None = Field(default=None)
selected_repo: str | None = Field(default=None)
model_config = {'extra': 'forbid'}

View File

@@ -12,11 +12,9 @@ import toml
from dotenv import load_dotenv
from pydantic import BaseModel, SecretStr, ValidationError
from openhands import __version__
from openhands.core import logger
from openhands.core.config.agent_config import AgentConfig
from openhands.core.config.app_config import AppConfig
from openhands.core.config.condenser_config import condenser_config_from_toml_section
from openhands.core.config.config_utils import (
OH_DEFAULT_AGENT,
OH_MAX_ITERATIONS,
@@ -195,44 +193,6 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
# Re-raise ValueError from SandboxConfig.from_toml_section
raise ValueError('Error in [sandbox] section in config.toml')
# Process condenser section if present
if 'condenser' in toml_config:
try:
# Pass the LLM configs to the condenser config parser
condenser_mapping = condenser_config_from_toml_section(
toml_config['condenser'], cfg.llms
)
# Assign the default condenser configuration to the default agent configuration
if 'condenser' in condenser_mapping:
# Get the default agent config and assign the condenser config to it
default_agent_config = cfg.get_agent_config()
default_agent_config.condenser = condenser_mapping['condenser']
logger.openhands_logger.debug(
'Default condenser configuration loaded from config toml and assigned to default agent'
)
except (TypeError, KeyError, ValidationError) as e:
logger.openhands_logger.warning(
f'Cannot parse [condenser] config from toml, values have not been applied.\nError: {e}'
)
# If no condenser section is in toml but enable_default_condenser is True,
# set LLMSummarizingCondenserConfig as default
elif cfg.enable_default_condenser:
from openhands.core.config.condenser_config import LLMSummarizingCondenserConfig
# Get default agent config
default_agent_config = cfg.get_agent_config()
# Create default LLM summarizing condenser config
default_condenser = LLMSummarizingCondenserConfig(
llm_config=cfg.get_llm_config(), # Use default LLM config
)
# Set as default condenser
default_agent_config.condenser = default_condenser
logger.openhands_logger.debug(
'Default LLM summarizing condenser assigned to default agent (no condenser in config)'
)
# Process extended section if present
if 'extended' in toml_config:
try:
@@ -243,15 +203,7 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
)
# Check for unknown sections
known_sections = {
'core',
'extended',
'agent',
'llm',
'security',
'sandbox',
'condenser',
}
known_sections = {'core', 'extended', 'agent', 'llm', 'security', 'sandbox'}
for key in toml_config:
if key.lower() not in known_sections:
logger.openhands_logger.warning(f'Unknown section [{key}] in {toml_file}')
@@ -540,6 +492,8 @@ def parse_arguments() -> argparse.Namespace:
args = parser.parse_args()
if args.version:
from openhands import __version__
print(f'OpenHands version: {__version__}')
sys.exit(0)

View File

@@ -6,21 +6,15 @@ import sys
import traceback
from datetime import datetime
from types import TracebackType
from typing import Any, Literal, Mapping, TextIO
from typing import Any, Literal, Mapping
import litellm
from pythonjsonlogger.json import JsonFormatter
from termcolor import colored
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
DEBUG = os.getenv('DEBUG', 'False').lower() in ['true', '1', 'yes']
DEBUG_LLM = os.getenv('DEBUG_LLM', 'False').lower() in ['true', '1', 'yes']
# Structured logs with JSON, disabled by default
LOG_JSON = os.getenv('LOG_JSON', 'False').lower() in ['true', '1', 'yes']
LOG_JSON_LEVEL_KEY = os.getenv('LOG_JSON_LEVEL_KEY', 'level')
# Configure litellm logging based on DEBUG_LLM
if DEBUG_LLM:
confirmation = input(
@@ -83,14 +77,7 @@ class StackInfoFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
if record.levelno >= logging.ERROR:
# LogRecord attributes are dynamically typed
# Capture the current stack trace as a string
stack = traceback.format_stack()
# Remove the last entries which are related to the logging machinery
stack = stack[:-3] # Adjust this number if needed
# Join the stack frames into a single string
stack_str = ''.join(stack)
setattr(record, 'stack_info', stack_str)
setattr(record, 'stack_info', True)
setattr(record, 'exc_info', sys.exc_info())
return True
@@ -300,36 +287,10 @@ def get_file_handler(
file_name = f'openhands_{timestamp}.log'
file_handler = logging.FileHandler(os.path.join(log_dir, file_name))
file_handler.setLevel(log_level)
if LOG_JSON:
file_handler.setFormatter(json_formatter())
else:
file_handler.setFormatter(file_formatter)
file_handler.setFormatter(file_formatter)
return file_handler
def json_formatter():
return JsonFormatter(
'{message}{levelname}',
style='{',
rename_fields={'levelname': LOG_JSON_LEVEL_KEY},
timestamp=True,
)
def json_log_handler(
level: int = logging.INFO,
_out: TextIO = sys.stdout,
) -> logging.Handler:
"""
Configure logger instance for structured logging as json lines.
"""
handler = logging.StreamHandler(_out)
handler.setLevel(level)
handler.setFormatter(json_formatter())
return handler
# Set up logging
logging.basicConfig(level=logging.ERROR)
@@ -367,11 +328,7 @@ if current_log_level == logging.DEBUG:
LOG_TO_FILE = True
openhands_logger.debug('DEBUG mode enabled.')
if LOG_JSON:
openhands_logger.addHandler(json_log_handler(current_log_level))
else:
openhands_logger.addHandler(get_console_handler(current_log_level))
openhands_logger.addHandler(get_console_handler(current_log_level))
openhands_logger.addFilter(SensitiveDataFilter(openhands_logger.name))
openhands_logger.propagate = False
openhands_logger.debug('Logging initialized')

View File

@@ -18,9 +18,9 @@ from openhands.core.schema import AgentState
from openhands.core.setup import (
create_agent,
create_controller,
create_memory,
create_runtime,
generate_sid,
initialize_repository_for_runtime,
)
from openhands.events import EventSource, EventStreamSubscriber
from openhands.events.action import MessageAction, NullAction
@@ -30,7 +30,6 @@ from openhands.events.observation import AgentStateChangedObservation
from openhands.events.serialization import event_from_dict
from openhands.io import read_input, read_task
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
class FakeUserResponseFunc(Protocol):
@@ -99,20 +98,12 @@ async def run_controller(
sid=sid,
headless_mode=headless_mode,
agent=agent,
selected_repository=config.sandbox.selected_repo,
)
# Connect to the runtime
call_async_from_sync(runtime.connect)
# Initialize repository if needed
if config.sandbox.selected_repo:
initialize_repository_for_runtime(
runtime,
agent=agent,
selected_repository=config.sandbox.selected_repo,
)
event_stream = runtime.event_stream
replay_events: list[Event] | None = None
if config.replay_trajectory_path:
logger.info('Trajectory replay is enabled')

View File

@@ -7,7 +7,6 @@ OpenHands uses its own `Message` class (`openhands/core/message.py`) which provi
## Class Structure
Our `Message` class (`openhands/core/message.py`):
```python
class Message(BaseModel):
role: Literal['user', 'system', 'assistant', 'tool']
@@ -23,14 +22,13 @@ class Message(BaseModel):
```
litellm's `Message` class (`litellm/types/utils.py`):
```python
class Message(OpenAIObject):
content: str | None
content: Optional[str]
role: Literal["assistant", "user", "system", "tool", "function"]
tool_calls: List[ChatCompletionMessageToolCall] | None
function_call: FunctionCall | None
audio: ChatCompletionAudioResponse | None = None
tool_calls: Optional[List[ChatCompletionMessageToolCall]]
function_call: Optional[FunctionCall]
audio: Optional[ChatCompletionAudioResponse] = None
```
## How It Works
@@ -38,7 +36,6 @@ class Message(OpenAIObject):
1. **Message Creation**: Our `Message` class is a Pydantic model that supports rich content (text and images) through its `content` field.
2. **Serialization**: The class uses Pydantic's `@model_serializer` to convert messages into dictionaries that litellm can understand. We have two serialization methods:
```python
def _string_serializer(self) -> dict:
# convert content to a single string
@@ -58,7 +55,6 @@ class Message(OpenAIObject):
```
The appropriate serializer is chosen based on the message's capabilities:
```python
@model_serializer
def serialize_model(self) -> dict:
@@ -68,13 +64,11 @@ class Message(OpenAIObject):
```
3. **Tool Call Handling**: Tool calls require special attention in serialization because:
- They need to work with litellm's API calls (which accept both dicts and objects)
- They need to be properly serialized for token counting
- They need to maintain compatibility with different LLM providers' formats
4. **litellm Integration**: When we pass our messages to `litellm.completion()`, litellm doesn't care about the message class type - it works with the dictionary representation. This works because:
- litellm's transformation code (e.g., `litellm/llms/anthropic/chat/transformation.py`) processes messages based on their structure, not their type
- our serialization produces dictionaries that match litellm's expected format
- litellm handles rich content by looking at the message structure, supporting both simple string content and lists of content items
@@ -84,7 +78,6 @@ class Message(OpenAIObject):
### Token Counting
To use litellm's token counter, we need to make sure that all message components (including tool calls) are properly serialized to dictionaries. This is because:
- litellm's token counter expects dictionary structures
- Tool calls need to be included in the token count
- Different providers may count tokens differently for structured content

View File

@@ -82,5 +82,8 @@ class ActionTypeSchema(BaseModel):
SEND_PR: str = Field(default='send_pr')
"""Send a PR to github."""
RECALL: str = Field(default='recall')
"""Retrieves data from a file or other storage."""
ActionType = ActionTypeSchema()

View File

@@ -49,5 +49,8 @@ class ObservationTypeSchema(BaseModel):
CONDENSE: str = Field(default='condense')
"""Result of a condensation operation."""
RECALL: str = Field(default='recall')
"""Result of a recall operation."""
ObservationType = ObservationTypeSchema()

View File

@@ -16,11 +16,13 @@ from openhands.core.logger import openhands_logger as logger
from openhands.events import EventStream
from openhands.events.event import Event
from openhands.llm.llm import LLM
from openhands.memory.memory import Memory
from openhands.microagent.microagent import BaseMicroAgent
from openhands.runtime import get_runtime_cls
from openhands.runtime.base import Runtime
from openhands.security import SecurityAnalyzer, options
from openhands.storage import get_file_store
from openhands.utils.async_utils import call_async_from_sync
def create_runtime(
@@ -28,19 +30,18 @@ def create_runtime(
sid: str | None = None,
headless_mode: bool = True,
agent: Agent | None = None,
selected_repository: str | None = None,
github_token: SecretStr | None = None,
) -> Runtime:
"""Create a runtime for the agent to run on.
Args:
config: The app config.
sid: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing.
Set it to incompatible value will cause unexpected behavior on RemoteRuntime.
headless_mode: Whether the agent is run in headless mode. `create_runtime` is typically called within evaluation scripts,
where we don't want to have the VSCode UI open, so it defaults to True.
agent: (optional) The agent instance to use for configuring the runtime.
Returns:
The created Runtime instance (not yet connected or initialized).
config: The app config.
sid: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing.
Set it to incompatible value will cause unexpected behavior on RemoteRuntime.
headless_mode: Whether the agent is run in headless mode. `create_runtime` is typically called within evaluation scripts,
where we don't want to have the VSCode UI open, so it defaults to True.
selected_repository: (optional) The GitHub repository to use.
github_token: (optional) The GitHub token to use.
"""
# if sid is provided on the command line, use it as the name of the event stream
# otherwise generate it on the basis of the configured jwt_secret
@@ -74,30 +75,8 @@ def create_runtime(
headless_mode=headless_mode,
)
logger.debug(
f'Runtime created with plugins: {[plugin.name for plugin in runtime.plugins]}'
)
call_async_from_sync(runtime.connect)
return runtime
def initialize_repository_for_runtime(
runtime: Runtime,
agent: Agent | None = None,
selected_repository: str | None = None,
github_token: SecretStr | None = None,
) -> str | None:
"""Initialize the repository for the runtime.
Args:
runtime: The runtime to initialize the repository for.
agent: (optional) The agent to load microagents for.
selected_repository: (optional) The GitHub repository to use.
github_token: (optional) The GitHub token to use.
Returns:
The repository directory path if a repository was cloned, None otherwise.
"""
# clone selected repository if provided
repo_directory = None
github_token = (
@@ -120,7 +99,44 @@ def initialize_repository_for_runtime(
agent.prompt_manager.load_microagents(microagents)
agent.prompt_manager.set_repository_info(selected_repository, repo_directory)
return repo_directory
logger.debug(
f'Runtime initialized with plugins: {[plugin.name for plugin in runtime.plugins]}'
)
return runtime
def create_memory(
microagents_dir: str,
agent: Agent,
runtime: Runtime,
event_stream: EventStream,
selected_repository: str | None = None,
) -> Memory:
# If the agent config has disabled microagents, use them
disabled_microagents = agent.config.disabled_microagents
mem = Memory(
event_stream=event_stream,
microagents_dir=microagents_dir,
disabled_microagents=disabled_microagents,
)
if agent.prompt_manager and runtime:
# sets available hosts
mem.set_runtime_info(runtime.web_hosts)
# loads microagents from repo/.openhands/microagents
microagents: list[BaseMicroAgent] = runtime.get_microagents_from_selected_repo(
selected_repository
)
mem.load_user_workspace_microagents(microagents)
if selected_repository:
repo_directory = selected_repository.split('/')[1]
if repo_directory:
mem.set_repository_info(selected_repository, repo_directory)
return mem
def create_agent(config: AppConfig) -> Agent:
@@ -132,6 +148,7 @@ def create_agent(config: AppConfig) -> Agent:
config=agent_config,
)
return agent

View File

@@ -1,5 +1,4 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
from openhands.core.schema import ActionType
@@ -34,26 +33,16 @@ class AgentSummarizeAction(Action):
return ret
class AgentFinishTaskCompleted(Enum):
FALSE = 'false'
PARTIAL = 'partial'
TRUE = 'true'
@dataclass
class AgentFinishAction(Action):
"""An action where the agent finishes the task.
Attributes:
final_thought (str): The message to send to the user.
task_completed (enum): Whether the agent believes the task has been completed.
outputs (dict): The other outputs of the agent, for instance "content".
outputs (dict): The outputs of the agent, for instance "content".
thought (str): The agent's explanation of its actions.
action (str): The action type, namely ActionType.FINISH.
"""
final_thought: str = ''
task_completed: AgentFinishTaskCompleted | None = None
outputs: dict[str, Any] = field(default_factory=dict)
thought: str = ''
action: str = ActionType.FINISH
@@ -106,3 +95,15 @@ class AgentDelegateAction(Action):
@property
def message(self) -> str:
return f"I'm asking {self.agent} for help with this task."
@dataclass
class RecallAction(Action):
# This action is used for retrieving data, e.g., from memory or a knowledge base.
query: dict[str, Any] = field(default_factory=dict)
thought: str = ''
action: str = ActionType.RECALL
@property
def message(self) -> str:
return f'Retrieved data for: {self.query}'

View File

@@ -27,6 +27,13 @@ class AgentCondensationObservation(Observation):
return self.content
@dataclass
class RecallObservation(Observation):
"""The output of a recall action."""
observation: str = ObservationType.RECALL
@dataclass
class AgentThinkObservation(Observation):
"""The output of a think action.

View File

@@ -62,12 +62,7 @@ class FileEditObservation(Observation):
new_content: str | None = None
observation: str = ObservationType.EDIT
impl_source: FileEditSource = FileEditSource.LLM_BASED_EDIT
diff: str | None = (
None # The raw diff between old and new content, used in OH_ACI mode
)
_diff_cache: str | None = (
None # Cache for the diff visualization, used in LLM-based editing mode
)
_diff_cache: str | None = None # Cache for the diff visualization
@property
def message(self) -> str:
@@ -131,7 +126,7 @@ class FileEditObservation(Observation):
n_context_lines: int = 2,
change_applied: bool = True,
) -> str:
"""Visualize the diff of the file edit. Used in the LLM-based editing mode.
"""Visualize the diff of the file edit.
Instead of showing the diff line by line, this function shows each hunk
of changes as a separate entity.

View File

@@ -8,6 +8,7 @@ from openhands.events.action.agent import (
AgentRejectAction,
AgentThinkAction,
ChangeAgentStateAction,
RecallAction,
)
from openhands.events.action.browse import BrowseInteractiveAction, BrowseURLAction
from openhands.events.action.commands import (
@@ -37,6 +38,7 @@ actions = (
AgentDelegateAction,
ChangeAgentStateAction,
MessageAction,
RecallAction,
)
ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in actions} # type: ignore[attr-defined]

View File

@@ -27,6 +27,7 @@ class EventStreamSubscriber(str, Enum):
RESOLVER = 'openhands_resolver'
SERVER = 'server'
RUNTIME = 'runtime'
MEMORY = 'memory'
MAIN = 'main'
TEST = 'test'

View File

@@ -21,12 +21,7 @@ class GitHubService:
token: SecretStr = SecretStr('')
refresh = False
def __init__(
self,
user_id: str | None = None,
idp_token: SecretStr | None = None,
token: SecretStr | None = None,
):
def __init__(self, user_id: str | None = None, token: SecretStr | None = None):
self.user_id = user_id
if token:
@@ -51,9 +46,6 @@ class GitHubService:
async def get_latest_token(self) -> SecretStr:
return self.token
async def get_latest_provider_token(self) -> SecretStr:
return self.token
async def _fetch_data(
self, url: str, params: dict | None = None
) -> tuple[Any, dict]:

View File

@@ -76,8 +76,6 @@ REASONING_EFFORT_SUPPORTED_MODELS = [
MODELS_WITHOUT_STOP_WORDS = [
'o1-mini',
'o1-preview',
'o1',
'o1-2024-12-17',
]
@@ -219,8 +217,9 @@ class LLM(RetryMixin, DebugMixin):
kwargs['stop'] = STOP_WORDS
mock_fncall_tools = kwargs.pop('tools')
# tool_choice should not be specified when mocking function calling
kwargs.pop('tool_choice', None)
kwargs['tool_choice'] = (
'none' # force no tool calling because we're mocking it - without it, it will cause issue with sglang
)
# if we have no messages, something went very wrong
if not messages:

View File

@@ -1,4 +1,4 @@
import openhands.memory.condenser.impl # noqa F401 (we import this to get the condensers registered)
from openhands.memory.condenser.condenser import Condenser, get_condensation_metadata
__all__ = ['Condenser', 'get_condensation_metadata', 'CONDENSER_REGISTRY']
__all__ = ['Condenser', 'get_condensation_metadata']

202
openhands/memory/memory.py Normal file
View File

@@ -0,0 +1,202 @@
from openhands.core.logger import openhands_logger as logger
from openhands.events.action.agent import RecallAction
from openhands.events.action.message import MessageAction
from openhands.events.event import Event, EventSource
from openhands.events.observation.agent import (
RecallObservation,
)
from openhands.events.stream import EventStream, EventStreamSubscriber
from openhands.microagent import (
BaseMicroAgent,
KnowledgeMicroAgent,
RepoMicroAgent,
load_microagents_from_dir,
)
from openhands.utils.prompt import PromptManager, RepositoryInfo, RuntimeInfo
class Memory:
"""
Memory is a component that listens to the EventStream for either user MessageAction (to create
a RecallAction) or a RecallAction (to produce a RecallObservation).
"""
def __init__(
self,
event_stream: EventStream,
microagents_dir: str,
disabled_microagents: list[str] | None = None,
):
self.event_stream = event_stream
self.microagents_dir = microagents_dir
self.disabled_microagents = disabled_microagents or []
# Subscribe to events
self.event_stream.subscribe(
EventStreamSubscriber.MEMORY,
self.on_event,
'Memory',
)
# Load global microagents (Knowledge + Repo).
self._load_global_microagents()
# Additional placeholders to store user workspace microagents if needed
self.repo_microagents: dict[str, RepoMicroAgent] = {}
self.knowledge_microagents: dict[str, KnowledgeMicroAgent] = {}
# Track whether we've seen the first user message
self._first_user_message_seen = False
# Store repository / runtime info to send them to the templating later
self.repository_info: RepositoryInfo | None = None
self.runtime_info: RuntimeInfo | None = None
# TODO: enable_prompt_extensions
def _load_global_microagents(self) -> None:
"""
Loads microagents from the global microagents_dir.
This is effectively what used to happen in PromptManager.
"""
repo_agents, knowledge_agents, _ = load_microagents_from_dir(
self.microagents_dir
)
for name, agent in knowledge_agents.items():
if name in self.disabled_microagents:
continue
if isinstance(agent, KnowledgeMicroAgent):
self.knowledge_microagents[name] = agent
for name, agent in repo_agents.items():
if name in self.disabled_microagents:
continue
if isinstance(agent, RepoMicroAgent):
self.repo_microagents[name] = agent
def set_repository_info(self, repo_name: str, repo_directory: str) -> None:
"""Store repository info so we can reference it in an observation."""
self.repository_info = RepositoryInfo(repo_name, repo_directory)
self.prompt_manager.set_repository_info(self.repository_info)
def set_runtime_info(self, runtime_hosts: dict[str, int]) -> None:
"""Store runtime info (web hosts, ports, etc.)."""
# e.g. { '127.0.0.1': 8080 }
self.runtime_info = RuntimeInfo(available_hosts=runtime_hosts)
self.prompt_manager.set_runtime_info(self.runtime_info)
def on_event(self, event: Event):
"""Handle an event from the event stream."""
if isinstance(event, MessageAction):
if event.source == 'user':
# If this is the first user message, create and add a RecallObservation
# with info about repo and runtime.
if not self._first_user_message_seen:
self._first_user_message_seen = True
self._on_first_user_message(event)
# continue with the next handler, to include microagents if suitable for this user message
self._on_user_message_action(event)
elif isinstance(event, RecallAction):
self._on_recall_action(event)
def _on_first_user_message(self, event: MessageAction):
"""Create and add to the stream a RecallObservation carrying info about repo and runtime."""
# Build the same text that used to be appended to the first user message
repo_instructions = ''
assert (
len(self.repo_microagents) <= 1
), f'Expecting at most one repo microagent, but found {len(self.repo_microagents)}: {self.repo_microagents.keys()}'
for microagent in self.repo_microagents.values():
# We assume these are the repo instructions
if repo_instructions:
repo_instructions += '\n\n'
repo_instructions += microagent.content
# Now wrap it in a RecallObservation, rather than altering the user message:
obs = RecallObservation(
content=self.prompt_manager.build_additional_info_text(repo_instructions)
)
self.event_stream.add_event(obs, EventSource.ENVIRONMENT)
def _on_user_message_action(self, event: MessageAction):
"""Replicates old microagent logic: if a microagent triggers on user text,
we embed it in an <extra_info> block and post a RecallObservation."""
if event.source != 'user':
return
# If there's no text, do nothing
user_text = event.content.strip()
if not user_text:
return
# Gather all triggered microagents
microagent_blocks = []
for name, agent in self.knowledge_microagents.items():
trigger = agent.match_trigger(user_text)
if trigger:
logger.info("Microagent '%s' triggered by keyword '%s'", name, trigger)
micro_text = (
f'<extra_info>\n'
f'The following information has been included based on a keyword match for "{trigger}". '
f"It may or may not be relevant to the user's request.\n\n"
f'{agent.content}\n'
f'</extra_info>'
)
microagent_blocks.append(micro_text)
if microagent_blocks:
# Combine all triggered microagents into a single RecallObservation
combined_text = '\n'.join(microagent_blocks)
obs = RecallObservation(content=combined_text)
self.event_stream.add_event(
obs, event.source if event.source else EventSource.ENVIRONMENT
)
def _on_recall_action(self, event: RecallAction):
"""If a RecallAction explicitly arrives, handle it."""
assert isinstance(event, RecallAction)
user_query = event.query.get('keywords', [])
matched_content = self.find_microagent_content(user_query)
obs = RecallObservation(content=matched_content)
self.event_stream.add_event(
obs, event.source if event.source else EventSource.ENVIRONMENT
)
def find_microagent_content(self, keywords: list[str]) -> str:
"""Replicate the same microagent logic."""
matched_texts: list[str] = []
for name, agent in self.knowledge_microagents.items():
for kw in keywords:
trigger = agent.match_trigger(kw)
if trigger:
logger.info(
"Microagent '%s' triggered by explicit RecallAction keyword '%s'",
name,
trigger,
)
block = (
f'<extra_info>\n'
f"(via RecallAction) Included knowledge from microagent '{name}', triggered by '{trigger}'\n\n"
f'{agent.content}\n'
f'</extra_info>'
)
matched_texts.append(block)
return '\n'.join(matched_texts)
def load_user_workspace_microagents(
self, user_microagents: list[BaseMicroAgent]
) -> None:
"""
If you want to load microagents from a user's cloned repo or workspace directory,
call this from agent_session or setup once the workspace is cloned.
"""
logger.info(
'Loading user workspace microagents: %s', [m.name for m in user_microagents]
)
for ma in user_microagents:
if ma.name in self.disabled_microagents:
continue
if isinstance(ma, KnowledgeMicroAgent):
self.knowledge_microagents[ma.name] = ma
elif isinstance(ma, RepoMicroAgent):
self.repo_microagents[ma.name] = ma
def set_prompt_manager(self, prompt_manager: PromptManager):
self.prompt_manager = prompt_manager

View File

@@ -202,7 +202,7 @@ async def process_issue(
timeout=300,
)
if os.getenv('GITLAB_CI') == 'true':
if os.getenv('GITLAB_CI') == 'True':
sandbox_config.local_runtime_url = os.getenv(
'LOCAL_RUNTIME_URL', 'http://localhost'
)
@@ -651,7 +651,7 @@ def main() -> None:
if not token:
raise ValueError('Token is required.')
platform = identify_token(token, repo)
platform = identify_token(token)
if platform == Platform.INVALID:
raise ValueError('Token is invalid.')

View File

@@ -22,37 +22,18 @@ class Platform(Enum):
GITLAB = 2
def identify_token(token: str, repo: str | None = None) -> Platform:
def identify_token(token: str) -> Platform:
"""
Identifies whether a token belongs to GitHub or GitLab.
Parameters:
token (str): The personal access token to check.
repo (str): Repository in format "owner/repo" for GitHub Actions token validation.
Returns:
Platform: "GitHub" if the token is valid for GitHub,
"GitLab" if the token is valid for GitLab,
"Invalid" if the token is not recognized by either.
"""
# Try GitHub Actions token format (Bearer) with repo endpoint if repo is provided
if repo:
github_repo_url = f'https://api.github.com/repos/{repo}'
github_bearer_headers = {
'Authorization': f'Bearer {token}',
'Accept': 'application/vnd.github+json',
}
try:
github_repo_response = requests.get(
github_repo_url, headers=github_bearer_headers, timeout=5
)
if github_repo_response.status_code == 200:
return Platform.GITHUB
except requests.RequestException as e:
print(f'Error connecting to GitHub API (repo check): {e}')
# Try GitHub PAT format (token)
github_url = 'https://api.github.com/user'
github_headers = {'Authorization': f'token {token}'}
@@ -63,7 +44,6 @@ def identify_token(token: str, repo: str | None = None) -> Platform:
except requests.RequestException as e:
print(f'Error connecting to GitHub API: {e}')
# Try GitLab token
gitlab_url = 'https://gitlab.com/api/v4/user'
gitlab_headers = {'Authorization': f'Bearer {token}'}

View File

@@ -25,7 +25,6 @@ from fastapi.security import APIKeyHeader
from openhands_aci.editor.editor import OHEditor
from openhands_aci.editor.exceptions import ToolError
from openhands_aci.editor.results import ToolResult
from openhands_aci.utils.diff import get_diff
from pydantic import BaseModel
from starlette.background import BackgroundTask
from starlette.exceptions import HTTPException as StarletteHTTPException
@@ -90,7 +89,7 @@ def _execute_file_editor(
new_str: str | None = None,
insert_line: int | None = None,
enable_linting: bool = False,
) -> tuple[str, tuple[str | None, str | None]]:
) -> str:
"""Execute file editor command and handle exceptions.
Args:
@@ -105,7 +104,7 @@ def _execute_file_editor(
enable_linting: Whether to enable linting
Returns:
tuple: A tuple containing the output string and a tuple of old and new file content
str: Result string from the editor operation
"""
result: ToolResult | None = None
try:
@@ -123,13 +122,13 @@ def _execute_file_editor(
result = ToolResult(error=e.message)
if result.error:
return f'ERROR:\n{result.error}', (None, None)
return f'ERROR:\n{result.error}'
if not result.output:
logger.warning(f'No output from file_editor for {path}')
return '', (None, None)
return ''
return result.output, (result.old_content, result.new_content)
return result.output
class ActionExecutor:
@@ -317,7 +316,7 @@ class ActionExecutor:
async def read(self, action: FileReadAction) -> Observation:
assert self.bash_session is not None
if action.impl_source == FileReadSource.OH_ACI:
result_str, _ = _execute_file_editor(
result_str = _execute_file_editor(
self.file_editor,
command='view',
path=action.path,
@@ -434,7 +433,7 @@ class ActionExecutor:
async def edit(self, action: FileEditAction) -> Observation:
assert action.impl_source == FileEditSource.OH_ACI
result_str, (old_content, new_content) = _execute_file_editor(
result_str = _execute_file_editor(
self.file_editor,
command=action.command,
path=action.path,
@@ -451,11 +450,6 @@ class ActionExecutor:
old_content=action.old_str,
new_content=action.new_str,
impl_source=FileEditSource.OH_ACI,
diff=get_diff(
old_contents=old_content or '',
new_contents=new_content or '',
filepath=action.path,
),
)
async def browse(self, action: BrowseURLAction) -> Observation:

View File

@@ -222,7 +222,7 @@ class Runtime(FileEditRuntimeMixin):
if isinstance(event, CmdRunAction):
if self.github_user_id and '$GITHUB_TOKEN' in event.command:
gh_client = GithubServiceImpl(user_id=self.github_user_id)
token = await gh_client.get_latest_provider_token()
token = await gh_client.get_latest_token()
if token:
export_cmd = CmdRunAction(
f"export GITHUB_TOKEN='{token.get_secret_value()}'"

View File

@@ -19,30 +19,19 @@ class DockerRuntimeBuilder(RuntimeBuilder):
version_info = self.docker_client.version()
server_version = version_info.get('Version', '').replace('-', '.')
self.is_podman = (
version_info.get('Components')[0].get('Name').startswith('Podman')
)
if (
tuple(map(int, server_version.split('.')[:2])) < (18, 9)
and not self.is_podman
):
if tuple(map(int, server_version.split('.')[:2])) < (18, 9):
raise AgentRuntimeBuildError(
'Docker server version must be >= 18.09 to use BuildKit'
)
if self.is_podman and tuple(map(int, server_version.split('.')[:2])) < (4, 9):
raise AgentRuntimeBuildError('Podman server version must be >= 4.9.0')
self.rolling_logger = RollingLogger(max_lines=10)
@staticmethod
def check_buildx(is_podman: bool = False):
def check_buildx():
"""Check if Docker Buildx is available"""
try:
result = subprocess.run(
['docker' if not is_podman else 'podman', 'buildx', 'version'],
capture_output=True,
text=True,
['docker', 'buildx', 'version'], capture_output=True, text=True
)
return result.returncode == 0
except FileNotFoundError:
@@ -79,18 +68,12 @@ class DockerRuntimeBuilder(RuntimeBuilder):
self.docker_client = docker.from_env()
version_info = self.docker_client.version()
server_version = version_info.get('Version', '').split('+')[0].replace('-', '.')
self.is_podman = (
version_info.get('Components')[0].get('Name').startswith('Podman')
)
if tuple(map(int, server_version.split('.'))) < (18, 9) and not self.is_podman:
if tuple(map(int, server_version.split('.'))) < (18, 9):
raise AgentRuntimeBuildError(
'Docker server version must be >= 18.09 to use BuildKit'
)
if self.is_podman and tuple(map(int, server_version.split('.'))) < (4, 9):
raise AgentRuntimeBuildError('Podman server version must be >= 4.9.0')
if not DockerRuntimeBuilder.check_buildx(self.is_podman):
if not DockerRuntimeBuilder.check_buildx():
# when running openhands in a container, there might not be a "docker"
# binary available, in which case we need to download docker binary.
# since the official openhands app image is built from debian, we use
@@ -127,7 +110,7 @@ class DockerRuntimeBuilder(RuntimeBuilder):
target_image_tag = tags[1].split(':')[1] if len(tags) > 1 else None
buildx_cmd = [
'docker' if not self.is_podman else 'podman',
'docker',
'buildx',
'build',
'--progress=plain',
@@ -156,7 +139,7 @@ class DockerRuntimeBuilder(RuntimeBuilder):
buildx_cmd.append(path) # must be last!
self.rolling_logger.start(
f'================ {buildx_cmd[0].upper()} BUILD STARTED ================'
'================ DOCKER BUILD STARTED ================'
)
try:

View File

@@ -2,80 +2,22 @@
[Daytona](https://www.daytona.io/) is a platform that provides a secure and elastic infrastructure for running AI-generated code. It provides all the necessary features for an AI Agent to interact with a codebase. It provides a Daytona SDK with official Python and TypeScript interfaces for interacting with Daytona, enabling you to programmatically manage development environments and execute code.
## Quick Start
## Getting started
### Step 1: Retrieve Your Daytona API Key
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
2. Click **"Create Key"**.
3. Enter a name for your key and confirm the creation.
4. Once the key is generated, copy it.
1. Sign in at https://app.daytona.io/
1. Generate and copy your API key
1. Set the following environment variables before running the OpenHands app on your local machine or via a `docker run` command:
### Step 2: Set Your API Key as an Environment Variable
Run the following command in your terminal, replacing `<your-api-key>` with the actual key you copied:
```bash
export DAYTONA_API_KEY="<your-api-key>"
RUNTIME="daytona"
DAYTONA_API_KEY="<your-api-key>"
```
This step ensures that OpenHands can authenticate with the Daytona platform when it runs.
### Step 3: Run OpenHands Locally Using Docker
To start the latest version of OpenHands on your machine, execute the following command in your terminal:
```bash
bash -i <(curl -sL https://get.daytona.io/openhands)
```
#### What This Command Does:
- Downloads the latest OpenHands release script.
- Runs the script in an interactive Bash session.
- Automatically pulls and runs the OpenHands container using Docker.
Once executed, OpenHands should be running locally and ready for use.
## Manual Initialization
### Step 1: Set the `OPENHANDS_VERSION` Environment Variable
Run the following command in your terminal, replacing `<openhands-release>` with the latest release's version seen in the [main README.md file](https://github.com/All-Hands-AI/OpenHands?tab=readme-ov-file#-quick-start):
Optionally, if you don't want your sandboxes to default to the US region, set:
```bash
export OPENHANDS_VERSION="<openhands-release>" # e.g. 0.27
```
### Step 2: Retrieve Your Daytona API Key
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
2. Click **"Create Key"**.
3. Enter a name for your key and confirm the creation.
4. Once the key is generated, copy it.
### Step 3: Set Your API Key as an Environment Variable:
Run the following command in your terminal, replacing `<your-api-key>` with the actual key you copied:
```bash
export DAYTONA_API_KEY="<your-api-key>"
```
### Step 4: Run the following `docker` command:
This command pulls and runs the OpenHands container using Docker. Once executed, OpenHands should be running locally and ready for use.
```bash
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:${OPENHANDS_VERSION}-nikolaik \
-e LOG_ALL_EVENTS=true \
-e RUNTIME=daytona \
-e DAYTONA_API_KEY=${DAYTONA_API_KEY} \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:${OPENHANDS_VERSION}
```
> **Tip:** If you don't want your sandboxes to default to the US region, you can set the `DAYTONA_TARGET` environment variable to `eu`
### Running OpenHands Locally Without Docker
Alternatively, if you want to run the OpenHands app on your local machine using `make run` without Docker, make sure to set the following environment variables first:
```bash
export RUNTIME="daytona"
export DAYTONA_API_KEY="<your-api-key>"
DAYTONA_TARGET="eu"
```
## Documentation

View File

@@ -201,29 +201,16 @@ class DockerRuntime(ActionExecutionClient):
port_mapping: dict[str, list[dict[str, str]]] | None = None
if not use_host_network:
port_mapping = {
f'{self._container_port}/tcp': [
{
'HostPort': str(self._host_port),
'HostIp': self.config.sandbox.runtime_binding_address,
}
],
f'{self._container_port}/tcp': [{'HostPort': str(self._host_port)}],
}
if self.vscode_enabled:
port_mapping[f'{self._vscode_port}/tcp'] = [
{
'HostPort': str(self._vscode_port),
'HostIp': self.config.sandbox.runtime_binding_address,
}
{'HostPort': str(self._vscode_port)}
]
for port in self._app_ports:
port_mapping[f'{port}/tcp'] = [
{
'HostPort': str(port),
'HostIp': self.config.sandbox.runtime_binding_address,
}
]
port_mapping[f'{port}/tcp'] = [{'HostPort': str(port)}]
else:
self.log(
'warn',
@@ -319,7 +306,6 @@ class DockerRuntime(ActionExecutionClient):
self.container = self.docker_client.containers.get(self.container_name)
if self.container.status == 'exited':
self.container.start()
config = self.container.attrs['Config']
for env_var in config['Env']:
if env_var.startswith('port='):
@@ -327,18 +313,11 @@ class DockerRuntime(ActionExecutionClient):
self._container_port = self._host_port
elif env_var.startswith('VSCODE_PORT='):
self._vscode_port = int(env_var.split('VSCODE_PORT=')[1])
self._app_ports = []
exposed_ports = config.get('ExposedPorts')
if exposed_ports:
for exposed_port in exposed_ports.keys():
exposed_port = int(exposed_port.split('/tcp')[0])
if (
exposed_port != self._host_port
and exposed_port != self._vscode_port
):
self._app_ports.append(exposed_port)
for exposed_port in config['ExposedPorts'].keys():
exposed_port = int(exposed_port.split('/tcp')[0])
if exposed_port != self._host_port and exposed_port != self._vscode_port:
self._app_ports.append(exposed_port)
self.api_url = f'{self.config.sandbox.local_runtime_url}:{self._container_port}'
self.log(
'debug',

View File

@@ -1,4 +1,4 @@
from typing import Callable
from typing import Callable, Optional
from openhands.core.config import AppConfig
from openhands.events.action import (
@@ -27,7 +27,7 @@ class E2BRuntime(Runtime):
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,
sandbox: E2BSandbox | None = None,
status_callback: Callable | None = None,
status_callback: Optional[Callable] = None,
):
super().__init__(
config,

View File

@@ -7,7 +7,7 @@ import shutil
import subprocess
import tempfile
import threading
from typing import Callable
from typing import Callable, Optional
import requests
import tenacity
@@ -155,7 +155,7 @@ class LocalRuntime(ActionExecutionClient):
self.api_url = f'{self.config.sandbox.local_runtime_url}:{self._host_port}'
self.status_callback = status_callback
self.server_process: subprocess.Popen[str] | None = None
self.server_process: Optional[subprocess.Popen[str]] = None
self.action_semaphore = threading.Semaphore(1) # Ensure one action at a time
# Update env vars

View File

@@ -1,5 +1,5 @@
import os
from typing import Callable
from typing import Callable, Optional
from urllib.parse import urlparse
import requests
@@ -42,7 +42,7 @@ class RemoteRuntime(ActionExecutionClient):
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,
env_vars: dict[str, str] | None = None,
status_callback: Callable | None = None,
status_callback: Optional[Callable] = None,
attach_to_existing: bool = False,
headless_mode: bool = True,
github_user_id: str | None = None,

View File

@@ -45,4 +45,4 @@ This extension is part of the OpenHands project. To modify or extend it:
## License
This extension is licensed under the MIT license.
This extension is licensed under the MIT license.

View File

@@ -4,30 +4,30 @@ const MemoryMonitor = require('./memory_monitor');
function activate(context) {
// Create memory monitor instance
const memoryMonitor = new MemoryMonitor();
// Store the context in the memory monitor
memoryMonitor.context = context;
// Register memory monitor start command
let startMonitorCommand = vscode.commands.registerCommand('openhands-memory-monitor.startMemoryMonitor', function () {
memoryMonitor.start();
});
// Register memory monitor stop command
let stopMonitorCommand = vscode.commands.registerCommand('openhands-memory-monitor.stopMemoryMonitor', function () {
memoryMonitor.stop();
});
// Register memory details command
let showMemoryDetailsCommand = vscode.commands.registerCommand('openhands-memory-monitor.showMemoryDetails', function () {
memoryMonitor.showDetails();
});
// Add all commands to subscriptions
context.subscriptions.push(startMonitorCommand);
context.subscriptions.push(stopMonitorCommand);
context.subscriptions.push(showMemoryDetailsCommand);
// Start memory monitoring by default
memoryMonitor.start();
}
@@ -39,4 +39,4 @@ function deactivate() {
module.exports = {
activate,
deactivate
}
}

View File

@@ -21,15 +21,15 @@ class MemoryMonitor {
this.isMonitoring = true;
this.statusBarItem.show();
// Initial update
this.updateMemoryInfo();
// Set interval for updates
this.intervalId = setInterval(() => {
this.updateMemoryInfo();
}, interval);
vscode.window.showInformationMessage('Memory monitoring started');
}
@@ -41,7 +41,7 @@ class MemoryMonitor {
this.isMonitoring = false;
clearInterval(this.intervalId);
this.statusBarItem.hide();
vscode.window.showInformationMessage('Memory monitoring stopped');
}
@@ -49,18 +49,18 @@ class MemoryMonitor {
const totalMem = os.totalmem();
const freeMem = os.freemem();
const usedMem = totalMem - freeMem;
// Calculate memory usage percentage
const memUsagePercent = Math.round((usedMem / totalMem) * 100);
// Format memory values to MB
const usedMemMB = Math.round(usedMem / (1024 * 1024));
const totalMemMB = Math.round(totalMem / (1024 * 1024));
// Update status bar
this.statusBarItem.text = `$(pulse) Mem: ${memUsagePercent}%`;
this.statusBarItem.tooltip = `Memory Usage: ${usedMemMB}MB / ${totalMemMB}MB`;
// Store memory data in history
this.memoryHistory.push({
timestamp: new Date(),
@@ -69,7 +69,7 @@ class MemoryMonitor {
memUsagePercent,
processMemory: process.memoryUsage()
});
// Limit history length
if (this.memoryHistory.length > this.maxHistoryLength) {
this.memoryHistory.shift();
@@ -86,7 +86,7 @@ class MemoryMonitor {
enableScripts: true
}
);
// Set up message handler for real-time updates
panel.webview.onDidReceiveMessage(
message => {
@@ -97,60 +97,60 @@ class MemoryMonitor {
undefined,
this.context ? this.context.subscriptions : []
);
// Initial update
this.updateWebviewContent(panel);
// Handle panel disposal
panel.onDidDispose(() => {
// Clean up any resources if needed
}, null, this.context ? this.context.subscriptions : []);
}
updateWebviewContent(panel) {
// Get system memory info
const totalMem = os.totalmem();
const freeMem = os.freemem();
const usedMem = totalMem - freeMem;
// Format memory values
const usedMemMB = Math.round(usedMem / (1024 * 1024));
const freeMemMB = Math.round(freeMem / (1024 * 1024));
const totalMemMB = Math.round(totalMem / (1024 * 1024));
// Get process memory usage
const processMemory = process.memoryUsage();
const rss = Math.round(processMemory.rss / (1024 * 1024));
const heapTotal = Math.round(processMemory.heapTotal / (1024 * 1024));
const heapUsed = Math.round(processMemory.heapUsed / (1024 * 1024));
// Get process information
this.processMonitor.getProcessInfo((error, processInfo) => {
if (error) {
console.error('Error getting process info:', error);
return;
}
// Create HTML content for the webview
const htmlContent = this.generateHtmlReport(
usedMemMB, freeMemMB, totalMemMB,
usedMemMB, freeMemMB, totalMemMB,
rss, heapTotal, heapUsed,
processInfo
);
// Set the webview's HTML content
panel.webview.html = htmlContent;
});
}
generateHtmlReport(usedMemMB, freeMemMB, totalMemMB, rss, heapTotal, heapUsed, processInfo) {
// Create memory usage history data for chart
const memoryLabels = this.memoryHistory.map((entry, index) => index);
const memoryData = this.memoryHistory.map(entry => entry.memUsagePercent);
const heapData = this.memoryHistory.map(entry =>
const heapData = this.memoryHistory.map(entry =>
Math.round(entry.processMemory.heapUsed / (1024 * 1024))
);
// Format process info table
let processTable = '';
if (processInfo && processInfo.processes) {
@@ -174,7 +174,7 @@ class MemoryMonitor {
</table>
`;
}
return `
<!DOCTYPE html>
<html lang="en">
@@ -237,7 +237,7 @@ class MemoryMonitor {
</head>
<body>
<h1>Memory Monitor</h1>
<div class="memory-card">
<h2>System Memory</h2>
<div class="memory-info">
@@ -259,7 +259,7 @@ class MemoryMonitor {
</div>
</div>
</div>
<div class="memory-card">
<h2>Process Memory (VSCode Extension Host)</h2>
<div class="memory-info">
@@ -277,18 +277,18 @@ class MemoryMonitor {
</div>
</div>
</div>
<div class="memory-card">
<h2>Memory Usage History</h2>
<div class="chart-container">
<canvas id="memoryChart"></canvas>
</div>
</div>
<div class="memory-card">
${processTable}
</div>
<script>
// Create memory usage chart
const ctx = document.getElementById('memoryChart').getContext('2d');
@@ -323,10 +323,10 @@ class MemoryMonitor {
}
}
});
// Set up real-time updates
const vscode = acquireVsCodeApi();
// Request updates every 5 seconds
setInterval(() => {
vscode.postMessage({
@@ -340,4 +340,4 @@ class MemoryMonitor {
}
}
module.exports = MemoryMonitor;
module.exports = MemoryMonitor;

Some files were not shown because too many files have changed in this diff Show More