Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,13 @@ RUN set -ux; \
if "$ACP_NODE_DIR/bin/npm" install -g \
@agentclientprotocol/claude-agent-acp@0.63.0 \
@agentclientprotocol/codex-acp@1.1.7 \
@google/gemini-cli@0.46.0; then \
@google/gemini-cli@0.46.0 \
pi-acp@0.0.33; then \
Comment thread
Deep070203 marked this conversation as resolved.
Outdated
# Create wrappers in /usr/local/bin that prepend ACP's Node 22 to PATH.
# This ensures the ACP binary's #!/usr/bin/env node shebang resolves
# to Node 22, while the repo's own node (NVM/system) stays untouched
# for tests.
for bin in claude-agent-acp codex-acp gemini; do \
for bin in claude-agent-acp codex-acp gemini pi-acp; do \
if [ -e "$ACP_NODE_DIR/bin/$bin" ]; then \
printf '#!/bin/sh\nPATH="%s/bin:$PATH" exec "%s/bin/%s" "$@"\n' \
"$ACP_NODE_DIR" "$ACP_NODE_DIR" "$bin" \
Expand Down
25 changes: 25 additions & 0 deletions openhands-sdk/openhands/sdk/settings/acp_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ class ACPProviderInfo:
ACPModelOption(id="gemini-2.5-flash", label="Gemini 2.5 Flash"),
)

_PI_MODELS: tuple[ACPModelOption, ...] = (
ACPModelOption(id="default", label="Default"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why default here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Unlike Claude Code, Codex, or Gemini CLI, pi-acp manages model resolution internally or via environment configuration.
Setting id="default" with default_model="default" allows the SDK to pass a clean, non-null model option while letting pi-acp use its native default engine setup

)


# ---------------------------------------------------------------------------
# Reserved file-content credential secrets for the built-in providers.
Expand Down Expand Up @@ -393,6 +397,7 @@ class ACPProviderInfo:
CLAUDE_AGENT_ACP_VERSION = "0.63.0"
CODEX_ACP_VERSION = "1.1.7"
GEMINI_CLI_VERSION = "0.46.0"
PI_ACP_VERSION = "0.0.33"
Comment thread
Deep070203 marked this conversation as resolved.
Outdated


ACP_PROVIDERS: Mapping[str, ACPProviderInfo] = MappingProxyType(
Expand Down Expand Up @@ -480,6 +485,26 @@ class ACPProviderInfo:
# ``~/.gemini`` (ignoring XDG), so only HOME relocates its state.
data_dir_env_var="HOME",
),
"pi": ACPProviderInfo(
key="pi",
display_name="Pi",
default_command=(
"npx",
"-y",
f"pi-acp@{PI_ACP_VERSION}",
),
api_key_env_var=None,
base_url_env_var=None,
default_session_mode="default",
Comment thread
Deep070203 marked this conversation as resolved.
Outdated
agent_name_patterns=("pi-acp", "pi"),
Comment thread
Deep070203 marked this conversation as resolved.
Outdated
supports_set_session_model=True,
supports_runtime_model_switch=True,
session_meta_key=None,
available_models=_PI_MODELS,
default_model="default",
binary_name="pi-acp",
data_dir_env_var="PI_CODING_AGENT_DIR",
),
}
)
"""Read-only registry of built-in ACP providers keyed by ``acp_server`` value."""
Expand Down
2 changes: 1 addition & 1 deletion openhands-sdk/openhands/sdk/settings/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1148,7 +1148,7 @@ def create_request(

AgentKind = Literal["openhands", "llm", "acp"]

ACPServerKind = Literal["claude-code", "codex", "gemini-cli", "custom"]
ACPServerKind = Literal["claude-code", "codex", "gemini-cli", "pi", "custom"]
"""Known ACP backend servers the GUI can pick from.

``custom`` means the user supplies the raw ``acp_command`` themselves;
Expand Down
11 changes: 11 additions & 0 deletions tests/sdk/agent/test_acp_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9164,6 +9164,17 @@ def test_claude_isolates_under_oauth_token(self, tmp_path):
)
assert Path(env["CLAUDE_CONFIG_DIR"]) == Path(persist) / "acp" / "claude-code"

def test_pi_isolates_data_dir(self, tmp_path):
agent = self._agent(["npx", "-y", "pi-acp"])
state = self._H._state(tmp_path)
persist = state.persistence_dir
assert persist is not None
with patch.dict("os.environ", {}, clear=True):
env = self._H._run_start(
agent, state, conn=self._H._make_conn(agent_name="pi-acp")
)
assert Path(env["PI_CODING_AGENT_DIR"]) == Path(persist) / "acp" / "pi"


# ---------------------------------------------------------------------------
# Secret masking (#1023)
Expand Down
28 changes: 25 additions & 3 deletions tests/sdk/settings/test_acp_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

class TestACPProviderInfo:
def test_known_providers_are_registered(self):
assert set(ACP_PROVIDERS) == {"claude-code", "codex", "gemini-cli"}
assert set(ACP_PROVIDERS) == {"claude-code", "codex", "gemini-cli", "pi"}

def test_all_entries_are_acp_provider_info(self):
for info in ACP_PROVIDERS.values():
Expand Down Expand Up @@ -92,6 +92,22 @@ def test_gemini_cli_metadata(self):
# Gemini CLI has no dedicated config-dir var, so only HOME relocates it.
assert info.data_dir_env_var == "HOME"

def test_pi_metadata(self):
info = ACP_PROVIDERS["pi"]
assert info.key == "pi"
assert info.display_name == "Pi"
assert info.default_command[0] == "npx"
assert "pi-acp" in info.default_command[-1]
assert info.api_key_env_var is None
assert info.base_url_env_var is None
assert info.default_session_mode == "default"
assert "pi-acp" in info.agent_name_patterns
assert info.supports_set_session_model is True
assert info.supports_runtime_model_switch is True
assert info.session_meta_key is None
assert info.binary_name == "pi-acp"
assert info.data_dir_env_var == "PI_CODING_AGENT_DIR"

def test_provider_info_is_frozen(self):
info = ACP_PROVIDERS["claude-code"]
with pytest.raises((AttributeError, TypeError)):
Expand Down Expand Up @@ -210,8 +226,14 @@ def test_every_provider_has_non_empty_session_mode(self):
)

def test_session_modes_are_distinct(self):
modes = [info.default_session_mode for info in ACP_PROVIDERS.values()]
assert len(modes) == len(set(modes)), "each provider should use a unique mode"
modes = [
info.default_session_mode
for info in ACP_PROVIDERS.values()
if info.default_session_mode != "default"
]
assert len(modes) == len(set(modes)), (
"permission-bypassing modes should be unique"
)

def test_detect_returns_matching_provider_for_all_registered_patterns(self):
"""Every registered pattern should resolve back to its own provider."""
Expand Down
8 changes: 4 additions & 4 deletions tests/sdk/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ def test_export_agent_settings_schema_emits_variant_tagged_sections() -> None:
server_field = next(f for f in acp_section.fields if f.key == "acp_server")
assert server_field.prominence is SettingProminence.CRITICAL
server_choices = {c.value for c in server_field.choices}
assert server_choices == {"claude-code", "codex", "gemini-cli", "custom"}
assert server_choices == {"claude-code", "codex", "gemini-cli", "pi", "custom"}

command_field = next(f for f in acp_section.fields if f.key == "acp_command")
assert command_field.prominence is SettingProminence.MINOR
Expand Down Expand Up @@ -1195,7 +1195,7 @@ def test_acp_create_agent_carries_provider_key() -> None:
directly (not from settings) defaults to ``None``; and the key survives a
serialization round-trip through the ``AgentBase`` discriminated union.
"""
for server in ("claude-code", "codex", "gemini-cli", "custom"):
for server in ("claude-code", "codex", "gemini-cli", "pi", "custom"):
kwargs: dict[str, Any] = {"acp_server": server}
if server == "custom":
kwargs["acp_command"] = ["my-acp"]
Expand All @@ -1218,7 +1218,7 @@ def test_acp_resolve_command_for_known_servers(
default stays the ``npx`` invocation.
"""
monkeypatch.setattr(shutil, "which", lambda _: None)
for server in ("claude-code", "codex", "gemini-cli"):
for server in ("claude-code", "codex", "gemini-cli", "pi"):
settings = ACPAgentSettings(acp_server=server)
cmd = settings.resolve_acp_command()
assert cmd, f"expected default command for {server}, got empty"
Expand Down Expand Up @@ -1992,7 +1992,7 @@ def test_acp_resolve_command_uses_registry_defaults(

# No pinned binary on PATH → registry npx default is returned verbatim.
monkeypatch.setattr(shutil, "which", lambda _: None)
for server_key in ("claude-code", "codex", "gemini-cli"):
for server_key in ("claude-code", "codex", "gemini-cli", "pi"):
settings = ACPAgentSettings(acp_server=server_key)
expected = list(ACP_PROVIDERS[server_key].default_command)
assert settings.resolve_acp_command() == expected
Expand Down