fix(agents): make Pi MCP eager and pin adapter (#3546)

Co-authored-by: cybertronic <cybertronic@gmail.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
Edan Binshtok
2026-09-09 14:50:40 -04:00
committed by GitHub
parent bf819a0228
commit 2afab22705
12 changed files with 55 additions and 32 deletions
@@ -14,6 +14,7 @@ resets it during startup.
| OpenCode | `opencode-ai` | `1.18.29` | `acp --print-logs --log-level ERROR` |
| Copilot | `@github/copilot` | `1.0.83` | `--acp` |
| Gemini | `@google/gemini-cli` | `0.58.0` | `--acp` |
| Pi | `pi-acp` | `0.0.33` | none |
Normal capability probes, sessions, container commands, and one-shot inference
use `npx --yes --prefer-offline package@effective-version` with the ACP
@@ -21,6 +21,7 @@ func TestManagedNPMRuntimeContracts(t *testing.T) {
{"opencode", NewOpenCodeACP(), "opencode-ai", []string{"acp", "--print-logs", "--log-level", "ERROR"}},
{"copilot", NewCopilotACP(), "@github/copilot", []string{"--acp"}},
{"gemini", NewGemini(), "@google/gemini-cli", []string{"--acp"}},
{"pi", NewPiACP(), "pi-acp", nil},
}
for _, tt := range tests {
@@ -151,6 +152,7 @@ func TestManagedAgentsHonorExactVersionCommandOption(t *testing.T) {
{"opencode", NewOpenCodeACP()},
{"copilot", NewCopilotACP()},
{"gemini", NewGemini()},
{"pi", NewPiACP()},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -3,5 +3,6 @@
"@agentclientprotocol/codex-acp": "1.10.0",
"@github/copilot": "1.0.83",
"@google/gemini-cli": "0.58.0",
"opencode-ai": "1.18.29"
"opencode-ai": "1.18.29",
"pi-acp": "0.0.33"
}
@@ -80,8 +80,8 @@ var newACPAgentSpecs = []struct {
}},
{func() Agent { return NewPiACP() }, acpAgentSpec{
id: "pi-acp", displayName: "Pi", detectBinaries: []string{"pi"},
expectedArgv: []string{"npx", "-y", "pi-acp"},
inferenceArgv: []string{"npx", "-y", "pi-acp"},
expectedArgv: []string{"npx", "--yes", "--prefer-offline", "pi-acp@0.0.33"},
inferenceArgv: []string{"npx", "--yes", "--prefer-offline", "pi-acp@0.0.33"},
passthroughArgv: []string{"pi"},
installViaNpm: true,
installScript: "npm install -g --ignore-scripts @earendil-works/pi-coding-agent",
+12 -7
View File
@@ -23,9 +23,10 @@ const (
)
var (
_ Agent = (*PiACP)(nil)
_ PassthroughAgent = (*PiACP)(nil)
_ InferenceAgent = (*PiACP)(nil)
_ Agent = (*PiACP)(nil)
_ PassthroughAgent = (*PiACP)(nil)
_ InferenceAgent = (*PiACP)(nil)
_ ManagedNPMRuntimeAgent = (*PiACP)(nil)
)
// PiACP implements Agent for the Pi Coding Agent via the pi-acp adapter.
@@ -71,7 +72,7 @@ func (a *PiACP) IsInstalled(ctx context.Context) (*DiscoveryResult, error) {
// The Pi package publishes the short `pi` binary used by passthrough. A
// non-interactive version check is a best-effort filter for unrelated tools
// with the same name; it does not prove package identity. The separate ACP
// probe validates the structured `npx -y pi-acp` surface, not this binary.
// probe validates the structured exact-version pi-acp surface, not this binary.
result, err := Detect(ctx, WithCommandCheck(piCLIBin, "--version"))
if err != nil {
return result, err
@@ -84,13 +85,17 @@ func (a *PiACP) IsInstalled(ctx context.Context) (*DiscoveryResult, error) {
}
func (a *PiACP) BuildCommand(opts CommandOptions) Command {
return Cmd("npx", "-y", piACPPkg).Build()
return a.ManagedNPMRuntime().ACPCommand(opts.ManagedRuntimeVersion)
}
func (a *PiACP) ManagedNPMRuntime() ManagedNPMRuntimeSpec {
return newManagedNPMRuntimeSpec(piACPPkg)
}
func (a *PiACP) Runtime() *RuntimeConfig {
canRecover := true
return &RuntimeConfig{
Cmd: Cmd("npx", "-y", piACPPkg).Build(),
Cmd: a.ManagedNPMRuntime().CachedACPCommand(),
WorkingDir: "{workspace}",
Env: map[string]string{},
ResourceLimits: ResourceLimits{MemoryMB: 4096, CPUCores: 2.0, Timeout: time.Hour},
@@ -123,7 +128,7 @@ func (a *PiACP) PermissionSettings() map[string]PermissionSetting {
func (a *PiACP) InferenceConfig() *InferenceConfig {
return &InferenceConfig{
Supported: true,
Command: NewCommand("npx", "-y", piACPPkg),
Command: a.ManagedNPMRuntime().CachedACPCommand(),
}
}
@@ -350,6 +350,7 @@ type piServerEntry struct {
Env map[string]string `json:"env,omitempty"`
URL string `json:"url,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Lifecycle string `json:"lifecycle,omitempty"`
}
func (PiStrategy) BuildPassthroughMCP(servers []types.McpServer, paths PassthroughPaths) (PassthroughArtifacts, error) {
@@ -362,9 +363,9 @@ func (PiStrategy) BuildPassthroughMCP(servers []types.McpServer, paths Passthrou
continue
}
if isStdioServer(srv) {
entries[srv.Name] = piServerEntry{Transport: string(ServerTypeStdio), Command: srv.Command, Args: srv.Args, Env: srv.Env}
entries[srv.Name] = piServerEntry{Transport: string(ServerTypeStdio), Command: srv.Command, Args: srv.Args, Env: srv.Env, Lifecycle: "eager"}
} else {
entries[srv.Name] = piServerEntry{Transport: piTransport(srv.Type), URL: srv.URL, Headers: srv.Headers}
entries[srv.Name] = piServerEntry{Transport: piTransport(srv.Type), URL: srv.URL, Headers: srv.Headers, Lifecycle: "eager"}
}
}
if len(entries) == 0 {
@@ -267,9 +267,15 @@ func TestPiStrategy_ProjectFileMerge(t *testing.T) {
if got.MCPServers["github"].Transport != "stdio" || got.MCPServers["github"].Command != "npx" || got.MCPServers["github"].Env["GITHUB_TOKEN"] != "tok" {
t.Errorf("github entry = %+v", got.MCPServers["github"])
}
if got.MCPServers["github"].Lifecycle != "eager" {
t.Errorf("github lifecycle = %q, want eager", got.MCPServers["github"].Lifecycle)
}
if got.MCPServers["stream"].Transport != "streamable-http" || got.MCPServers["stream"].URL != "https://x/mcp" {
t.Errorf("stream entry = %+v", got.MCPServers["stream"])
}
if got.MCPServers["stream"].Lifecycle != "eager" {
t.Errorf("stream lifecycle = %q, want eager", got.MCPServers["stream"].Lifecycle)
}
}
func TestPiStrategy_RemoteTransportMapping(t *testing.T) {
@@ -17,8 +17,10 @@ gives Kandev a non-global injection point.
Pi uses a dedicated `mcpconfig.PiStrategy` that writes or merges
`<workspace>/.pi/mcp.json` under the `mcpServers` key. The strategy emits Pi's
remote transport spelling (`streamable-http`) and preserves existing user
settings and servers when the file already exists.
remote transport spelling (`streamable-http`) and marks Kandev-supplied servers
`eager` so their session-scoped tools are registered when the session starts.
It preserves existing user settings and servers when the file already exists;
the lifecycle of unrelated user-supplied servers is not changed.
For passthrough sessions, Pi declares the strategy on `PassthroughConfig`, using
the existing passthrough MCP materialization path. For ACP sessions, Pi declares
+7 -5
View File
@@ -8,9 +8,10 @@ status: complete
## Overview
Pi's built-in definition assigns `npx -y pi-acp` to both structured ACP
execution and interactive passthrough. Registry metadata confirms that
`pi-acp` publishes the ACP adapter binary, while
Pi's built-in definition assigns the managed command
`npx --yes --prefer-offline pi-acp@<effective-version>` to structured ACP
execution, while interactive passthrough uses `pi`. Registry metadata confirms
that `pi-acp` publishes the ACP adapter binary, while
`@earendil-works/pi-coding-agent` publishes the interactive `pi` binary. The
repair keeps the ACP adapter on structured and inference paths, launches `pi`
for passthrough, and makes installation and discovery agree on that executable.
@@ -29,7 +30,7 @@ and the observable behavior is specified in
- Update `apps/backend/internal/agent/agents/pi_acp.go` so
`BuildCommand`, `Runtime().Cmd`, and `InferenceConfig().Command` remain
`npx -y pi-acp`.
`npx --yes --prefer-offline pi-acp@<effective-version>`.
- Change `PassthroughConfig.PassthroughCmd` to the globally installed `pi`
executable. Do not route the passthrough path through managed ACP package or
version resolution.
@@ -50,7 +51,8 @@ and the observable behavior is specified in
## Tests
- **What:** Structured Pi chat and inference remain on `npx -y pi-acp`, while
- **What:** Structured Pi chat and inference use
`npx --yes --prefer-offline pi-acp@<effective-version>`, while
`PassthroughCmd` is exactly `pi`; the install recipe and detection binary
match the interactive package.
**File:** `apps/backend/internal/agent/agents/new_acp_agents_test.go`.
@@ -59,8 +59,8 @@ None.
## Acceptance Criteria
- Structured `BuildCommand`, `Runtime().Cmd`, and one-shot inference remain
`npx -y pi-acp`.
- Structured `BuildCommand`, `Runtime().Cmd`, and one-shot inference use
`npx --yes --prefer-offline pi-acp@<effective-version>`.
- Passthrough lifecycle command resolution returns `pi`, including the path
that materializes Pi's project MCP configuration.
- Pi discovery requires `pi`, and installation runs exactly
@@ -89,7 +89,8 @@ regression, and documentation describe one coupled behavior.
because adapter-only installation cannot run passthrough.
- The package is installed with `--ignore-scripts` exactly as reported. The
implementation must not relax or remove that safety flag.
- No package version-management support is added for `pi-acp` in this repair.
- Managed version selection applies to structured ACP and inference launches;
passthrough remains on the separately installed `pi` executable.
## Output Contract
+4 -3
View File
@@ -34,7 +34,8 @@ Antigravity has no automated install: Google distributes `agy_acp_server.par` (`
Pi uses separate executables for its two Kandev modes:
- Structured ACP sessions and one-shot inference use `npx -y pi-acp`.
- Structured ACP sessions and one-shot inference use
`npx --yes --prefer-offline pi-acp@<effective-version>`.
- CLI Passthrough starts the globally installed `pi` executable.
- The Pi install action runs `npm install -g --ignore-scripts @earendil-works/pi-coding-agent`.
@@ -52,8 +53,8 @@ The status shown on this page is authoritative for the current host. A CLI that
### Update a managed agent runtime
The update icon is available on managed Claude, Codex, OpenCode, Copilot, and
Gemini agent cards. It updates the runtime on the Kandev host.
The update icon is available on managed Claude, Codex, OpenCode, Copilot,
Gemini, and Pi agent cards. It updates the runtime on the Kandev host.
Each managed runtime has a reviewed Kandev default. If you have not selected a
version, Kandev uses that exact default for probes, sessions, standalone
@@ -2,7 +2,7 @@
status: draft
system: cli
created: 2026-05-16
updated: 2026-08-16
updated: 2026-09-09
owners:
- cfl
---
@@ -26,7 +26,7 @@ Anthropic has announced that **agent-SDK / `claude -p` usage will draw from a pa
- **AC-CLI-CLI-MODE-PARITY-001.4:** AND Kandev does not launch `pi-acp` or `npx` as the passthrough process
- **AC-CLI-CLI-MODE-PARITY-001.5:** GIVEN a Pi profile with `cli_passthrough: false`
- **AC-CLI-CLI-MODE-PARITY-001.6:** WHEN Kandev starts structured chat or one-shot inference
- **AC-CLI-CLI-MODE-PARITY-001.7:** THEN the command remains `npx -y pi-acp`
- **AC-CLI-CLI-MODE-PARITY-001.7:** THEN the command uses `npx --yes --prefer-offline pi-acp@<effective-version>`
- **AC-CLI-CLI-MODE-PARITY-001.8:** GIVEN Pi is unavailable on the Kandev host
## Migrated source detail
@@ -53,9 +53,11 @@ the ACP adapter. CLI passthrough launches the interactive CLI directly under
the PTY; managed ACP runtime resolution does not replace that passthrough
command.
For Pi, structured chat and inference use `npx -y pi-acp`, while CLI
passthrough launches the globally installed `pi` executable. Kandev's Pi
install action runs
For Pi, structured chat and inference use
`npx --yes --prefer-offline pi-acp@<effective-version>`, while CLI passthrough
launches the globally installed `pi` executable. The effective version comes
from the operator selection or the reviewed Kandev default, `0.0.33`. Kandev's
Pi install action runs
`npm install -g --ignore-scripts @earendil-works/pi-coding-agent`, and agent
discovery treats a `pi` executable on the Kandev process `PATH` that passes its
non-interactive `--version` check as the installation signal.
@@ -106,7 +108,7 @@ Users can still press Ctrl-C directly inside the xterm terminal. A dedicated too
### Pi keeps its ACP adapter for structured execution
- GIVEN a Pi profile with `cli_passthrough: false`
- WHEN Kandev starts structured chat or one-shot inference
- THEN the command remains `npx -y pi-acp`
- THEN the command uses `npx --yes --prefer-offline pi-acp@<effective-version>`
### Pi installation provisions the passthrough executable
- GIVEN Pi is unavailable on the Kandev host
@@ -218,7 +220,6 @@ Pressing Ctrl-C inside the passthrough terminal writes `\x03` to PTY stdin. A fu
- Parsing PTY output into `task_messages` (transcriber). The terminal panel shows live output; the chat transcript only shows user-sent text in CLI mode.
- Billing-type / subscription-quota badge or DTO surface. That belongs to `subscription-usage.md`.
- Adding passthrough support to agents that don't have it (`Supported: false`).
- Adding `pi-acp` to the managed runtime update catalogue.
- Migrating away from the current ACP bridge.
- Headless `-p` mode for Claude (drains API credit; we deliberately do not use it).