From 2afab227052d5eec394ea38f7013a314ecc6b449 Mon Sep 17 00:00:00 2001 From: Edan Binshtok Date: Wed, 9 Sep 2026 14:50:40 -0400 Subject: [PATCH] fix(agents): make Pi MCP eager and pin adapter (#3546) Co-authored-by: cybertronic Co-authored-by: Carlos Florencio --- .../agent/agents/ACP_BRIDGE_VERSIONS.md | 1 + .../agent/agents/managed_npm_runtime_test.go | 2 ++ .../agents/managed_npm_runtime_versions.json | 3 ++- .../agent/agents/new_acp_agents_test.go | 4 ++-- apps/backend/internal/agent/agents/pi_acp.go | 19 ++++++++++++------- .../internal/agent/mcpconfig/passthrough.go | 5 +++-- .../agent/mcpconfig/passthrough_test.go | 6 ++++++ .../0020-pi-project-mcp-config-injection.md | 6 ++++-- docs/plans/pi-runtime-surfaces/plan.md | 12 +++++++----- .../task-01-correct-pi-execution-surfaces.md | 7 ++++--- docs/public/agents-and-profiles.md | 7 ++++--- .../specs/cli/requirements/cli-mode-parity.md | 15 ++++++++------- 12 files changed, 55 insertions(+), 32 deletions(-) diff --git a/apps/backend/internal/agent/agents/ACP_BRIDGE_VERSIONS.md b/apps/backend/internal/agent/agents/ACP_BRIDGE_VERSIONS.md index 39b02cac27..4500f93f08 100644 --- a/apps/backend/internal/agent/agents/ACP_BRIDGE_VERSIONS.md +++ b/apps/backend/internal/agent/agents/ACP_BRIDGE_VERSIONS.md @@ -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 diff --git a/apps/backend/internal/agent/agents/managed_npm_runtime_test.go b/apps/backend/internal/agent/agents/managed_npm_runtime_test.go index b8282a9602..536419de9d 100644 --- a/apps/backend/internal/agent/agents/managed_npm_runtime_test.go +++ b/apps/backend/internal/agent/agents/managed_npm_runtime_test.go @@ -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) { diff --git a/apps/backend/internal/agent/agents/managed_npm_runtime_versions.json b/apps/backend/internal/agent/agents/managed_npm_runtime_versions.json index a05b6d46ae..464cd74f8a 100644 --- a/apps/backend/internal/agent/agents/managed_npm_runtime_versions.json +++ b/apps/backend/internal/agent/agents/managed_npm_runtime_versions.json @@ -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" } diff --git a/apps/backend/internal/agent/agents/new_acp_agents_test.go b/apps/backend/internal/agent/agents/new_acp_agents_test.go index bdb9974180..0701d9bd2a 100644 --- a/apps/backend/internal/agent/agents/new_acp_agents_test.go +++ b/apps/backend/internal/agent/agents/new_acp_agents_test.go @@ -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", diff --git a/apps/backend/internal/agent/agents/pi_acp.go b/apps/backend/internal/agent/agents/pi_acp.go index 695ab6914e..cfb25fd18e 100644 --- a/apps/backend/internal/agent/agents/pi_acp.go +++ b/apps/backend/internal/agent/agents/pi_acp.go @@ -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(), } } diff --git a/apps/backend/internal/agent/mcpconfig/passthrough.go b/apps/backend/internal/agent/mcpconfig/passthrough.go index 8c805e41a3..9ea606e0de 100644 --- a/apps/backend/internal/agent/mcpconfig/passthrough.go +++ b/apps/backend/internal/agent/mcpconfig/passthrough.go @@ -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 { diff --git a/apps/backend/internal/agent/mcpconfig/passthrough_test.go b/apps/backend/internal/agent/mcpconfig/passthrough_test.go index a2cd0275ea..be58a631fb 100644 --- a/apps/backend/internal/agent/mcpconfig/passthrough_test.go +++ b/apps/backend/internal/agent/mcpconfig/passthrough_test.go @@ -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) { diff --git a/docs/decisions/0020-pi-project-mcp-config-injection.md b/docs/decisions/0020-pi-project-mcp-config-injection.md index f3b90a3bae..01586a6fd2 100644 --- a/docs/decisions/0020-pi-project-mcp-config-injection.md +++ b/docs/decisions/0020-pi-project-mcp-config-injection.md @@ -17,8 +17,10 @@ gives Kandev a non-global injection point. Pi uses a dedicated `mcpconfig.PiStrategy` that writes or merges `/.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 diff --git a/docs/plans/pi-runtime-surfaces/plan.md b/docs/plans/pi-runtime-surfaces/plan.md index 9fa9bcc77b..fea36290bf 100644 --- a/docs/plans/pi-runtime-surfaces/plan.md +++ b/docs/plans/pi-runtime-surfaces/plan.md @@ -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@` 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@`. - 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@`, 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`. diff --git a/docs/plans/pi-runtime-surfaces/task-01-correct-pi-execution-surfaces.md b/docs/plans/pi-runtime-surfaces/task-01-correct-pi-execution-surfaces.md index 8903198634..99f83870ae 100644 --- a/docs/plans/pi-runtime-surfaces/task-01-correct-pi-execution-surfaces.md +++ b/docs/plans/pi-runtime-surfaces/task-01-correct-pi-execution-surfaces.md @@ -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@`. - 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 diff --git a/docs/public/agents-and-profiles.md b/docs/public/agents-and-profiles.md index 5aab6ba901..0911b29b38 100644 --- a/docs/public/agents-and-profiles.md +++ b/docs/public/agents-and-profiles.md @@ -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@`. - 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 diff --git a/docs/specs/cli/requirements/cli-mode-parity.md b/docs/specs/cli/requirements/cli-mode-parity.md index 5b70032fe8..8bb3216280 100644 --- a/docs/specs/cli/requirements/cli-mode-parity.md +++ b/docs/specs/cli/requirements/cli-mode-parity.md @@ -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@` - **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@`, 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@` ### 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).