Skip to content

Commit b70717c

Browse files
Simon Rosenbergclaude
andcommitted
feat(acp): carry provider key onto ACPAgent via create_agent
The authoritative ACP provider key lives on ACPAgentSettings.acp_server ('claude-code', 'codex', 'gemini-cli', 'custom'), but create_agent() dropped it when building the runtime ACPAgent — which kept only acp_command. The launch command does not reliably reverse-map to a provider (detect_acp_provider_by_command returns None for the common `npx -y @zed-industries/claude-code-acp` and `npx -y @openai/codex acp` forms), so downstream consumers had no dependable way to recover the provider from a serialized agent. Add an optional `acp_server: str | None` field to ACPAgent, populated by create_agent() from the settings. It then rides ConversationInfo.agent, giving the conversation UI an authoritative key to resolve a provider brand label / model list without reverse-parsing the command or hand-stamping a tag. Additive and backward-compatible: defaults to None for agents built directly (not from settings) and for older serialized payloads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 735e9be commit b70717c

3 files changed

Lines changed: 44 additions & 0 deletions

File tree

openhands-sdk/openhands/sdk/agent/acp_agent.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,6 +1270,20 @@ class ACPAgent(AgentBase):
12701270
" ['npx', '-y', '@agentclientprotocol/claude-agent-acp']"
12711271
),
12721272
)
1273+
acp_server: str | None = Field(
1274+
default=None,
1275+
description=(
1276+
"Provider registry key identifying which ACP CLI this agent runs "
1277+
"('claude-code', 'codex', 'gemini-cli', or 'custom'); None when the "
1278+
"agent is built directly rather than via ACPAgentSettings. Set by "
1279+
"ACPAgentSettings.create_agent() from ACPAgentSettings.acp_server so "
1280+
"the authoritative key survives onto the agent — and thus onto "
1281+
"ConversationInfo.agent — because the launch command in acp_command "
1282+
"does not reliably reverse-map to a provider. Informational only: "
1283+
"consumers use it to resolve a provider brand label / model list; "
1284+
"the subprocess is still launched from acp_command."
1285+
),
1286+
)
12731287
acp_args: list[str] = Field(
12741288
default_factory=list,
12751289
description="Additional arguments for the ACP server command",

openhands-sdk/openhands/sdk/settings/model.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1724,6 +1724,11 @@ def create_agent(self) -> ACPAgent:
17241724
return ACPAgent(
17251725
llm=self.llm,
17261726
acp_command=self.resolve_acp_command(),
1727+
# Carry the authoritative provider key onto the agent: acp_command
1728+
# alone does not reliably reverse-map to a provider, so consumers
1729+
# (e.g. the conversation UI resolving a brand label / model list)
1730+
# read it from ConversationInfo.agent.acp_server.
1731+
acp_server=self.acp_server,
17271732
acp_args=list(self.acp_args),
17281733
# Pass acp_env directly rather than via resolve_acp_env() so the
17291734
# deprecation warning is not emitted twice on the create_agent path:

tests/sdk/test_settings.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,31 @@ def test_acp_create_agent_uses_server_default_command(
886886
"@agentclientprotocol/claude-agent-acp@0.30.0",
887887
]
888888
assert agent.acp_model == "claude-opus-4-6"
889+
# The authoritative provider key is carried onto the agent.
890+
assert agent.acp_server == "claude-code"
891+
892+
893+
def test_acp_create_agent_carries_provider_key() -> None:
894+
"""``create_agent`` stamps the provider key onto the agent for every choice.
895+
896+
``acp_command`` does not reliably reverse-map to a provider, so the key must
897+
ride the agent (and thus ``ConversationInfo.agent``) for consumers to resolve
898+
a brand label / model list. Custom carries through verbatim; an agent built
899+
directly (not from settings) defaults to ``None``; and the key survives a
900+
serialization round-trip through the ``AgentBase`` discriminated union.
901+
"""
902+
for server in ("claude-code", "codex", "gemini-cli", "custom"):
903+
kwargs: dict[str, Any] = {"acp_server": server}
904+
if server == "custom":
905+
kwargs["acp_command"] = ["my-acp"]
906+
agent = ACPAgentSettings(**kwargs).create_agent()
907+
assert agent.acp_server == server
908+
909+
assert ACPAgent(acp_command=["x"]).acp_server is None
910+
911+
agent = ACPAgentSettings(acp_server="gemini-cli").create_agent()
912+
reloaded = ACPAgent.model_validate_json(agent.model_dump_json())
913+
assert reloaded.acp_server == "gemini-cli"
889914

890915

891916
def test_acp_resolve_command_for_known_servers(

0 commit comments

Comments
 (0)