Skip to content

Commit ca621a4

Browse files
Enable ACPAgent on RemoteRuntime API (#2190)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ab83d05 commit ca621a4

File tree

12 files changed

+786
-54
lines changed

12 files changed

+786
-54
lines changed

.github/workflows/run-eval.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,18 @@ on:
9797
- gemini
9898
- gpt5
9999
- planning
100+
agent_type:
101+
description: >-
102+
Agent type: 'default' for standard Agent,
103+
'acp-claude' for ACPAgent with Claude Code,
104+
'acp-codex' for ACPAgent with Codex.
105+
required: false
106+
default: default
107+
type: choice
108+
options:
109+
- default
110+
- acp-claude
111+
- acp-codex
100112

101113

102114
env:
@@ -319,6 +331,7 @@ jobs:
319331
ENABLE_CONVERSATION_EVENT_LOGGING: ${{ github.event.inputs.enable_conversation_event_logging || false }}
320332
MAX_RETRIES: ${{ github.event.inputs.max_retries || '3' }}
321333
TOOL_PRESET: ${{ github.event.inputs.tool_preset || 'default' }}
334+
AGENT_TYPE: ${{ github.event.inputs.agent_type || 'default' }}
322335
TRIGGERED_BY: ${{ github.actor }}
323336
run: |
324337
echo "Dispatching evaluation workflow with SDK commit: $SDK_SHA (benchmark: $BENCHMARK, eval branch: $EVAL_BRANCH, benchmarks branch: $BENCHMARKS_BRANCH, tool preset: $TOOL_PRESET)"
@@ -337,8 +350,9 @@ jobs:
337350
--argjson enable_conversation_event_logging "$ENABLE_CONVERSATION_EVENT_LOGGING" \
338351
--arg max_retries "$MAX_RETRIES" \
339352
--arg tool_preset "$TOOL_PRESET" \
353+
--arg agent_type "$AGENT_TYPE" \
340354
--arg triggered_by "$TRIGGERED_BY" \
341-
'{ref: $ref, inputs: {sdk_commit: $sdk, eval_limit: $eval_limit, models_json: ($models | tostring), trigger_reason: $reason, pr_number: $pr, benchmarks_branch: $benchmarks, benchmark: $benchmark, instance_ids: $instance_ids, num_infer_workers: $num_infer_workers, num_eval_workers: $num_eval_workers, enable_conversation_event_logging: $enable_conversation_event_logging, max_retries: $max_retries, tool_preset: $tool_preset, triggered_by: $triggered_by}}')
355+
'{ref: $ref, inputs: {sdk_commit: $sdk, eval_limit: $eval_limit, models_json: ($models | tostring), trigger_reason: $reason, pr_number: $pr, benchmarks_branch: $benchmarks, benchmark: $benchmark, instance_ids: $instance_ids, num_infer_workers: $num_infer_workers, num_eval_workers: $num_eval_workers, enable_conversation_event_logging: $enable_conversation_event_logging, max_retries: $max_retries, tool_preset: $tool_preset, agent_type: $agent_type, triggered_by: $triggered_by}}')
342356
RESPONSE=$(curl -sS -o /tmp/dispatch.out -w "%{http_code}" -X POST \
343357
-H "Authorization: token $PAT_TOKEN" \
344358
-H "Accept: application/vnd.github+json" \

examples/01_standalone_sdk/40_acp_agent_example.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
88
Prerequisites:
99
- Node.js / npx available
10-
- Claude Code CLI authenticated (or CLAUDE_API_KEY set)
10+
- ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY set (can point to LiteLLM proxy)
1111
1212
Usage:
1313
uv run python examples/01_standalone_sdk/40_acp_agent_example.py
@@ -38,6 +38,9 @@
3838
"Based on what you just saw, which agent class is the newest addition?"
3939
)
4040
print(f"ask_agent response: {response}")
41+
# Report cost (ACP server reports usage via session_update notifications)
42+
cost = agent.llm.metrics.accumulated_cost
43+
print(f"EXAMPLE_COST: {cost:.4f}")
4144
finally:
4245
# Clean up the ACP server subprocess
4346
agent.close()
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Example: ACPAgent with Remote Runtime via API.
2+
3+
This example demonstrates running an ACPAgent (Claude Code via ACP protocol)
4+
in a remote sandboxed environment via Runtime API. It follows the same pattern
5+
as 04_convo_with_api_sandboxed_server.py but uses ACPAgent instead of the
6+
default LLM-based Agent.
7+
8+
Usage:
9+
uv run examples/02_remote_agent_server/09_acp_agent_with_remote_runtime.py
10+
11+
Requirements:
12+
- LLM_BASE_URL: LiteLLM proxy URL (routes Claude Code requests)
13+
- LLM_API_KEY: LiteLLM virtual API key
14+
- RUNTIME_API_KEY: API key for runtime API access
15+
"""
16+
17+
import os
18+
import time
19+
20+
from openhands.sdk import (
21+
Conversation,
22+
RemoteConversation,
23+
get_logger,
24+
)
25+
from openhands.sdk.agent import ACPAgent
26+
from openhands.workspace import APIRemoteWorkspace
27+
28+
29+
logger = get_logger(__name__)
30+
31+
32+
# ACP agents (Claude Code) route through LiteLLM proxy
33+
llm_base_url = os.getenv("LLM_BASE_URL")
34+
llm_api_key = os.getenv("LLM_API_KEY")
35+
assert llm_base_url and llm_api_key, "LLM_BASE_URL and LLM_API_KEY required"
36+
37+
# Set ANTHROPIC_* vars so Claude Code routes through LiteLLM
38+
os.environ["ANTHROPIC_BASE_URL"] = llm_base_url
39+
os.environ["ANTHROPIC_API_KEY"] = llm_api_key
40+
41+
runtime_api_key = os.getenv("RUNTIME_API_KEY")
42+
assert runtime_api_key, "RUNTIME_API_KEY required"
43+
44+
# If GITHUB_SHA is set (e.g. running in CI of a PR), use that to ensure consistency
45+
# Otherwise, use the latest image from main
46+
server_image_sha = os.getenv("GITHUB_SHA") or "main"
47+
server_image = f"ghcr.io/openhands/agent-server:{server_image_sha[:7]}-python-amd64"
48+
logger.info(f"Using server image: {server_image}")
49+
50+
with APIRemoteWorkspace(
51+
runtime_api_url=os.getenv("RUNTIME_API_URL", "https://runtime.eval.all-hands.dev"),
52+
runtime_api_key=runtime_api_key,
53+
server_image=server_image,
54+
image_pull_policy="Always",
55+
target_type="binary", # CI builds binary target images
56+
forward_env=["ANTHROPIC_BASE_URL", "ANTHROPIC_API_KEY"],
57+
) as workspace:
58+
agent = ACPAgent(
59+
acp_command=["claude-agent-acp"], # Pre-installed in Docker image
60+
)
61+
62+
received_events: list = []
63+
last_event_time = {"ts": time.time()}
64+
65+
def event_callback(event) -> None:
66+
received_events.append(event)
67+
last_event_time["ts"] = time.time()
68+
69+
conversation = Conversation(
70+
agent=agent, workspace=workspace, callbacks=[event_callback]
71+
)
72+
assert isinstance(conversation, RemoteConversation)
73+
74+
try:
75+
conversation.send_message(
76+
"List the files in /workspace and describe what you see."
77+
)
78+
conversation.run()
79+
80+
while time.time() - last_event_time["ts"] < 2.0:
81+
time.sleep(0.1)
82+
83+
# Report cost
84+
cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost
85+
print(f"EXAMPLE_COST: {cost:.4f}")
86+
finally:
87+
conversation.close()

openhands-agent-server/openhands/agent_server/api.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
from openhands.agent_server.tool_router import tool_router
3939
from openhands.agent_server.vscode_router import vscode_router
4040
from openhands.agent_server.vscode_service import get_vscode_service
41+
from openhands.sdk.agent import (
42+
ACPAgent, # noqa: F401 — register in DiscriminatedUnionMixin
43+
)
4144
from openhands.sdk.logger import DEBUG, get_logger
4245

4346

openhands-agent-server/openhands/agent_server/docker/Dockerfile

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,22 @@ RUN set -eux; \
8585
chown -R ${USERNAME}:${USERNAME} /workspace; \
8686
rm -rf /var/lib/apt/lists/*
8787

88-
# NOTE: we should NOT include UV_PROJECT_ENVIRONMENT here,
88+
# Pre-install ACP servers for ACPAgent support (Claude Code + Codex)
89+
# Install Node.js/npm if not present (SWE-bench base images may lack them)
90+
RUN set -eux; \
91+
if ! command -v npm >/dev/null 2>&1; then \
92+
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
93+
apt-get install -y --no-install-recommends nodejs && \
94+
rm -rf /var/lib/apt/lists/*; \
95+
fi; \
96+
npm install -g @zed-industries/claude-agent-acp @zed-industries/codex-acp
97+
98+
# Configure Claude Code managed settings for headless operation:
99+
# Allow all tool permissions (no human in the loop to approve).
100+
RUN mkdir -p /etc/claude-code && \
101+
echo '{"permissions":{"allow":["Edit","Read","Bash"]}}' > /etc/claude-code/managed-settings.json
102+
103+
# NOTE: we should NOT include UV_PROJECT_ENVIRONMENT here,
89104
# since the agent might use it to perform other work (e.g. tools that use Python)
90105
COPY --from=ghcr.io/astral-sh/uv /uv /uvx /bin/
91106

openhands-agent-server/openhands/agent_server/event_service.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
StoredConversation,
1212
)
1313
from openhands.agent_server.pub_sub import PubSub, Subscriber
14-
from openhands.sdk import LLM, Agent, AgentBase, Event, Message, get_logger
14+
from openhands.sdk import LLM, AgentBase, Event, Message, get_logger
1515
from openhands.sdk.conversation.impl.local_conversation import LocalConversation
1616
from openhands.sdk.conversation.secret_registry import SecretValue
1717
from openhands.sdk.conversation.state import (
@@ -429,7 +429,8 @@ async def start(self):
429429
workspace = self.stored.workspace
430430
assert isinstance(workspace, LocalWorkspace)
431431
Path(workspace.working_dir).mkdir(parents=True, exist_ok=True)
432-
agent = Agent.model_validate(
432+
agent_cls = type(self.stored.agent)
433+
agent = agent_cls.model_validate(
433434
self.stored.agent.model_dump(context={"expose_secrets": True}),
434435
)
435436

0 commit comments

Comments
 (0)