Skip to content

Enable ACPAgent on RemoteRuntime API via v2 conversations contract#2461

Closed
simonrosenberg wants to merge 2 commits intomainfrom
feat/acp-remote-runtime-v2-api
Closed

Enable ACPAgent on RemoteRuntime API via v2 conversations contract#2461
simonrosenberg wants to merge 2 commits intomainfrom
feat/acp-remote-runtime-v2-api

Conversation

@simonrosenberg
Copy link
Collaborator

@simonrosenberg simonrosenberg commented Mar 16, 2026

Summary

  • replay the ACP remote-runtime support from Enable ACPAgent on RemoteRuntime API #2190 onto current main
  • preserve the existing /api/conversations REST contract as the v1 Agent-only shape
  • add ACP-capable conversation endpoints under /api/v2/conversations and route RemoteConversation there automatically for ACPAgent

Why this approach

#2190 was useful, but it widened the public v1 REST contract by eagerly registering ACPAgent at the AgentBase boundary. That changed the OpenAPI schema and caused older clients that posted a plain agent object without kind to start failing with 422.

This PR keeps the old contract working in place and introduces ACP support through a versioned conversations API instead of changing v1 in place:

  • /api/conversations remains backward-compatible and Agent-only
  • /api/v2/conversations supports both Agent and ACPAgent
  • ACP remote-runtime SDK calls automatically use v2, so the new functionality is available without breaking existing REST clients

Included from #2190

  • ACPAgent remote-runtime support and lifecycle cleanup
  • ACP Docker image provisioning and examples
  • ACP eval workflow plumbing
  • ACP runtime fixes and tests from the original feature branch

Test plan

  • uv run pytest tests/agent_server/test_conversation_router.py tests/agent_server/test_conversation_router_v2.py tests/agent_server/test_openapi_discriminator.py tests/sdk/conversation/remote/test_remote_conversation.py tests/sdk/agent/test_acp_agent.py
  • uv run pre-commit run --files openhands-agent-server/openhands/agent_server/models.py openhands-agent-server/openhands/agent_server/conversation_service.py openhands-agent-server/openhands/agent_server/conversation_router_v2.py openhands-agent-server/openhands/agent_server/api.py openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py tests/agent_server/test_conversation_router.py tests/agent_server/test_conversation_router_v2.py tests/agent_server/test_openapi_discriminator.py tests/sdk/conversation/remote/test_remote_conversation.py

Closes the gap between #2190 and #2451 by reintroducing the feature behind a versioned REST contract.


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22 Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:d7461a9-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-d7461a9-python \
  ghcr.io/openhands/agent-server:d7461a9-python

All tags pushed for this build

ghcr.io/openhands/agent-server:d7461a9-golang-amd64
ghcr.io/openhands/agent-server:d7461a9-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:d7461a9-golang-arm64
ghcr.io/openhands/agent-server:d7461a9-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:d7461a9-java-amd64
ghcr.io/openhands/agent-server:d7461a9-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:d7461a9-java-arm64
ghcr.io/openhands/agent-server:d7461a9-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:d7461a9-python-amd64
ghcr.io/openhands/agent-server:d7461a9-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-amd64
ghcr.io/openhands/agent-server:d7461a9-python-arm64
ghcr.io/openhands/agent-server:d7461a9-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-arm64
ghcr.io/openhands/agent-server:d7461a9-golang
ghcr.io/openhands/agent-server:d7461a9-java
ghcr.io/openhands/agent-server:d7461a9-python

About Multi-Architecture Support

  • Each variant tag (e.g., d7461a9-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., d7461a9-python-amd64) are also available if needed

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions
Copy link
Contributor

github-actions bot commented Mar 16, 2026

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Contributor

github-actions bot commented Mar 16, 2026

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

Copy link
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taste Rating: 🟡 Acceptable - Solid versioning strategy, but webhook contract change needs human review.

Verdict: ✅ Worth merging after addressing webhook concern. Core logic is sound, comprehensive tests, backward-compatible REST API.

Key Insight: The v1/v2 API split elegantly solves the ACP integration without breaking existing clients, but webhook notifications now use the v2 schema universally—verify this is acceptable for existing webhook subscribers before merging.

_compose_conversation_info_v2(event_service.stored, state)
if use_v2
else _compose_conversation_info_v1(event_service.stored, state)
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Important: Webhook contract breaking change.

The _notify_conversation_webhooks function now always sends ConversationInfoV2 format (via _compose_conversation_info_v2), even for v1 conversations. This means webhook subscribers will receive the polymorphic ACPEnabledAgent type in all notifications.

Impact:

  • Subscribers doing strict schema validation against the old ConversationInfo type may break
  • The change from agent: Agent to agent: Agent | ACPAgent is backward compatible for JSON parsing, but not for strict OpenAPI validation

Recommendation:

  • Document this webhook schema change in the PR description
  • Consider notifying known webhook users before merging
  • Or: maintain separate webhook formats for v1 vs v2 conversations (more complex but fully backward compatible)

Is this intentional? Should webhooks preserve v1 format for v1 conversations?

# LimitOverrunError, silently killing the filter/receive pipeline and
# leaving the prompt() future unresolved forever. 100 MiB is generous
# enough for any realistic message while still bounding memory.
_STREAM_READER_LIMIT: int = 100 * 1024 * 1024 # 100 MiB
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Document memory impact of 100MB buffer.

The increase from 64KB to 100MB is well-justified (large tool outputs), but worth noting:

  • Each ACP agent instance allocates this per subprocess stream (stdin, stdout, stderr filtered reader)
  • In multi-agent scenarios (e.g., agent-server handling multiple conversations), this could add up to significant memory overhead

Consider adding an environment variable like ACP_STREAM_BUFFER_LIMIT to make this configurable for memory-constrained deployments. Not blocking, but would improve operational flexibility.

# Configure Claude Code managed settings for headless operation:
# Allow all tool permissions (no human in the loop to approve).
RUN mkdir -p /etc/claude-code && \
echo '{"permissions":{"allow":["Edit","Read","Bash"]}}' > /etc/claude-code/managed-settings.json
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Nit: Verify managed-settings.json path.

Is /etc/claude-code/managed-settings.json the documented path for Claude Code managed settings? A comment with a reference to Claude Code docs would help future maintainers verify this is correct.


agent: ACPEnabledAgent


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Acceptable: StoredConversation schema evolution handled correctly.

StoredConversation now extends StartConversationRequestV2 (polymorphic agent), but this is backward compatible:

  • ACPEnabledAgent = Agent | ACPAgent means existing Agent instances still validate
  • Pydantic discriminator handles both types correctly
  • No migration needed for existing persisted conversations

Good type design here. 👍

@github-actions
Copy link
Contributor

github-actions bot commented Mar 16, 2026

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   api.py1681292%74, 86, 101, 107, 278, 281, 285–287, 289, 295, 336
   conversation_router_v2.py1095945%85–87, 101, 114, 127–128, 153–156, 167–170, 185–187, 189–193, 199, 201, 213–216, 218, 220–222, 235–239, 252–256, 268–271, 288, 291–293, 306–309, 321–324
   conversation_service.py4219776%102–103, 130, 133, 135, 142–148, 176, 183, 204, 266, 284, 303, 309, 314, 320, 328–329, 338–341, 350, 364–366, 373, 398–399, 438, 441, 458–462, 464–465, 468–469, 472–477, 557, 564–568, 571–572, 576–580, 583–584, 588–592, 595–596, 602–607, 614–615, 619, 621–622, 627–628, 634–635, 642–643, 647–649, 667, 691, 923, 926
   event_service.py3208174%55–56, 74–76, 85–89, 92–95, 115, 219, 236, 290–291, 295, 303, 306, 352–353, 369, 371, 375–377, 381, 390–391, 393, 397, 403, 405, 413–418, 555, 557–558, 562, 576–578, 580, 584–587, 591–594, 602–605, 625, 629–634, 646–647, 649–650, 657–658, 660–661, 665, 671, 688–689
openhands-sdk/openhands/sdk/agent
   acp_agent.py3897879%194–196, 250–253, 255–256, 283, 285, 289, 295, 306–307, 312, 379, 481–482, 493, 498, 529, 539, 544, 555–558, 564–566, 569–571, 573, 575–576, 578, 580, 585, 594–595, 599–600, 604, 611–617, 627–632, 634, 643–645, 648–649, 655–659, 661, 663–664, 672, 709, 713–714, 958–959
   base.py1932288%200, 257–259, 289, 293–297, 345–347, 357, 367, 375–376, 486, 523–524, 534–535
openhands-sdk/openhands/sdk/conversation/impl
   local_conversation.py4002693%288, 293, 321, 364, 382, 398, 463, 641–642, 645, 797, 805, 807, 811–812, 823, 825–827, 852, 924, 1050, 1054, 1124, 1131–1132
   remote_conversation.py60510682%69, 71, 142, 169, 182, 184–187, 197, 219–220, 225–228, 311, 324–326, 332, 373, 514–517, 519, 539–543, 548–551, 554, 566–570, 713–714, 718–719, 733, 756–757, 776, 787–788, 808–811, 813–814, 838–840, 843–847, 849–850, 854, 856–864, 866, 903, 1033, 1101–1102, 1106, 1111–1115, 1121–1127, 1140–1141, 1224, 1231, 1237–1238, 1297–1298, 1316–1317
TOTAL20896523574% 

Co-authored-by: openhands <openhands@all-hands.dev>
@simonrosenberg
Copy link
Collaborator Author

Closing this PR since it's a duplicate of #2460

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants