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:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user