@estebanforge/pi-antigravity-bridge 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,9 @@ How the provider works internally. For build/test/debug workflow see [DEVELOPMEN
4
4
 
5
5
  ## Turn engine
6
6
 
7
- The provider runs one turn engine: a long-lived `agy --input-format stream-json --output-format stream-json` process per provider. Turns are fed over stdin; agy emits NDJSON events on stdout; the driver parses them and streams text into pi token by token. Conversation binding comes from the `init` event, tool steps arrive as typed events (no protobuf decoding), and token usage is live.
7
+ The provider ships two turn engines behind one contract (`TurnDriver`,
8
+ `src/driver-types.ts`): the default **stream-json engine** (below) and the
9
+ opt-in **ACP engine** (bottom of this doc). Turns are fed over stdin; agy emits NDJSON events on stdout; the driver parses them and streams text into pi token by token. Conversation binding comes from the `init` event, tool steps arrive as typed events (no protobuf decoding), and token usage is live.
8
10
 
9
11
  Shared infrastructure: session binding (`sessions.json`), runtime config, the `AskAntigravity` tool, the MCP tool bridge surface, and the G1 context digest (off by default - see below).
10
12
 
@@ -21,10 +23,15 @@ src/patch-cleanup.ts detects a leftover invokeTool patch from pre-1.3.0 install
21
23
  src/discovery.ts conversation-id binding for the AskAntigravity one-shot tool (agy -p never prints its conversation id)
22
24
  src/models.ts agy models -> pi Model projection (full catalog, per-model effort)
23
25
  src/sessions.ts atomic JSON store: pi session -> agy conversation + watermark
24
- src/config.ts persisted runtime config (bridgeTools, digest, mode, permissions, model/thinking defaults)
26
+ src/config.ts persisted runtime config (engine + acp block, bridgeTools, digest, mode, permissions, model/thinking defaults)
25
27
  src/ask-tool.ts the AskAntigravity one-shot delegation tool (model/thinking defaults)
26
28
  src/mcp-server.ts MCP tool bridge server: ferries tools/list + tools/call; calls park in the provider round-trip
27
- src/diff-render.ts render agy's file edits as git diffs in pi's thinking stream
29
+ src/diff-render.ts stream-json: render agy's file edits as git diffs in pi's thinking stream; formatInlineDiff (no git) renders ACP's native diffs
30
+ src/driver-types.ts TurnDriver contract shared by both engines (request/handle/snapshot types)
31
+ src/acp/jsonrpc.ts NDJSON JSON-RPC 2.0 framing with line buffering and typed error results
32
+ src/acp/connection.ts ACP server process + protocol (initialize, session/new+load, prompt with image/resource blocks, config options, cancel probing, auto permissions)
33
+ src/acp/events.ts session/update -> DriverActivity mapping (pure; probe-frame regressions pinned)
34
+ src/acp/driver.ts AcpDriver: serialized turns, remaining-budget timer pause, Gate D abort, connection-scoped exit handling, reconnect/agentInfo snapshots
28
35
  ```
29
36
 
30
37
  No generated protobuf code, no SQLite dependency.
@@ -58,3 +65,43 @@ By default the prompt agy receives is only the latest user message: agy keeps it
58
65
  ### Removed: the legacy-sqlite engine (1.3.2)
59
66
 
60
67
  The pre-1.3.0 engine (spawn `agy -p`, poll the SQLite conversation DB, decode protobuf step payloads) was removed in 1.3.2. agy 1.1.18 changed the step-row storage to a two-phase write (a metadata-only placeholder row that grows in place), which the polling decoder read once as an empty placeholder and never re-read: turns completed with the full reply in the database and zero text streamed to pi (issue #1, reported by @imatimba). The engine reverse-engineered an undocumented storage format, so every agy storage change risked repeating that silent failure. The stream-json engine shares none of that code path and is unaffected by storage-format changes. `AGY_ENGINE` and the `engine` config key are gone; a stale value in an existing `config.json` is ignored.
68
+
69
+ ## ACP engine (opt-in, official server)
70
+
71
+ `config.engine: "acp"` (or `/agy engine acp`) routes turns through Google's
72
+ official ACP server (`agy_acp_server.par`, registry id `antigravity-acp`)
73
+ over JSON-RPC stdio. Off by default; stream-json remains the default and a
74
+ supported secondary — phase-4 deletion is conditioned on upstream shipping
75
+ usage fields (Gate B), per docs/ACP-ADOPTION-PLAN.md.
76
+
77
+ Modules: `src/acp/jsonrpc.ts` (framing/correlation), `src/acp/connection.ts`
78
+ (process + protocol methods + in-connection `auto` permission answering),
79
+ `src/acp/events.ts` (update mapping, pure), `src/acp/driver.ts`
80
+ (`AcpDriver`). Both engines implement `TurnDriver`; `provider.ts` depends on
81
+ the interface only and is otherwise unchanged.
82
+
83
+ Phase-2/3 additions (all ACP-only, verified live):
84
+
85
+ - **Images**: pi image attachments ride as typed content blocks in the
86
+ prompt array; models advertise `input: ["text","image"]` only when the
87
+ engine is `acp` (decided at extension load). stream-json stays text-only.
88
+ - **Digest delivery**: with `config.digest` on, ACP ships the G1 digest as
89
+ a native `embeddedContext` resource block (images → resource → text);
90
+ stream-json keeps it inline. Same cache churn either way.
91
+ - **Tool display (Gate C)**: ACP tool steps render as thinking labels -
92
+ native re-exec and wrapper replay are retired on ACP turns (the server
93
+ already executed the tool). Edits render their server-supplied diff
94
+ (`tool_call content[]` → `formatInlineDiff`, no git subprocesses).
95
+ - **Diagnostics**: the ACP snapshot reports reconnects (connections beyond
96
+ the first: Gate D kills + stale-exit replacements) and the handshake
97
+ `agentInfo` name/title; `/agy doctor` surfaces both.
98
+
99
+ Engine-specific behavior: session ids are scoped per engine
100
+ (`sid:<x>@acp`); model/effort ship as one full slug via
101
+ `session/set_config_option` and are RE-APPLIED after every server restart
102
+ (config does not persist); `session/load` history replay is swallowed (never
103
+ live text); abort is teardown+kill+reload while `session/cancel` is
104
+ unimplemented (RC01); usage tokens are absent (zero-usage fallback).
105
+ Verified protocol shapes and the auth flow:
106
+ docs/ACP-PROTOCOL-REFERENCE.md. Raw captures: `probe-logs/` (gitignored,
107
+ local only).
@@ -34,6 +34,35 @@ npm run smoke:pi
34
34
  # quota. Proves the persistent process: init binds a conversation, text deltas
35
35
  # arrive, the result settles, and a second turn reuses the process.
36
36
  AGY_LIVE=1 node --experimental-strip-types scripts/smoke-stream-json.mjs
37
+
38
+ # Live smoke for the ACP engine through OUR driver stack. OPT-IN: spends a
39
+ # little quota. Needs AGY_ACP_BIN (or acp on PATH) and a one-time
40
+ # /agy acp-auth credential setup.
41
+ AGY_ACP_LIVE=1 AGY_ACP_BIN=~/.local/opt/agy-acp/current/agy_acp_server.par \
42
+ npx tsx scripts/smoke-acp.mjs
43
+
44
+ # Live smoke for the Gate F bridge e2e: the real ACP server lists the bridge
45
+ # catalog and completes a tool call through the registered mcpServers entry.
46
+ AGY_ACP_LIVE=1 AGY_ACP_BIN=~/.local/opt/agy-acp/current/agy_acp_server.par \
47
+ npx tsx scripts/smoke-acp-bridge.mjs
48
+
49
+ # Live smoke for image prompts on the ACP engine: builds a 64x64 two-tone PNG
50
+ # in-process and asserts the model identifies both halves through the full
51
+ # driver stack.
52
+ AGY_ACP_LIVE=1 AGY_ACP_BIN=~/.local/opt/agy-acp/current/agy_acp_server.par \
53
+ npx tsx scripts/smoke-acp-image.mjs
54
+
55
+ # Live probe: thought-chunk sparsity, tool_call content[]/rawInput shapes,
56
+ # and the /plan command flow, captured to probe-logs/ (local only).
57
+ AGY_ACP_LIVE=1 AGY_ACP_BIN=~/.local/opt/agy-acp/current/agy_acp_server.par \
58
+ npx tsx scripts/probe-acp-phase2.mjs
59
+
60
+ # Live parity run: the SAME scenario set (streaming, continuity, bridge
61
+ # round-trip, effort switch, serialization, abort+recover, usage) through
62
+ # BOTH engines. Needs the agy CLI AND the ACP binary. Spends ~13 flash-low
63
+ # turns; prints a per-scenario matrix and exits non-zero on any mismatch.
64
+ AGY_ACP_LIVE=1 AGY_ACP_BIN=~/.local/opt/agy-acp/current/agy_acp_server.par \
65
+ npx tsx scripts/parity-live.mjs
37
66
  ```
38
67
 
39
68
  ## Debugging a hang or "stuck" turn
@@ -52,6 +81,10 @@ Most "stuck" reports trace to one of:
52
81
  - `tests/provider-digest.test.ts` - the G1 context digest builder: injects pi-side context without replaying agy's own history.
53
82
  - `tests/patch-cleanup.test.ts` - legacy-patch detection and restore, real fs via tmpdirs, no mocks.
54
83
  - `tests/mcp-server.test.ts` - the MCP tool bridge end-to-end against a real (port 0) server: capability gate, per-pid config lifecycle, shared-secret token gate, 1 MB body cap, protocol-version clamp. The provider owns the tool catalog and the round-trip; the server only ferries list/call.
84
+ - `tests/acp-jsonrpc.test.ts` - the JSON-RPC stdio session: id correlation, typed error results, server-to-client requests, notifications, line framing (partial frames buffered across chunk boundaries, garbage lines counted not fatal).
85
+ - `tests/acp-events.test.ts` - ACP session/update mapping onto pi activities (text, thought, tool cards) and the session/load replay suppression.
86
+ - `tests/acp-driver.test.ts` - the ACP driver over the fake server (`tests/helpers/fake-acp-server.mjs`, scenario-selected): happy flow, load-replay, permission auto-answer, Gate D abort (cancel probe, teardown, `cancelSupported` memory), the stale-exit race (a killed connection's late exit must not fail its replacement - `ACP_FAKE_SLOW_DEATH_MS`), auth errors, park/kickIdle timer pause with remaining budget.
87
+ - `tests/acp-config.test.ts` - engine selection narrowing (`AGY_ENGINE`/`config.engine`), acp block parsing.
55
88
 
56
89
  ## Module map
57
90
 
@@ -1,127 +1,51 @@
1
1
  # pi-antigravity-bridge: capability gaps
2
2
 
3
- Status of the MCP tool bridge between agy (Antigravity CLI, used as pi's Gemini
4
- provider) and pi's extension/builtin tools. This doc tracks **open gaps only**.
5
- Shipped work lives in `CHANGELOG.md` (most recently, 1.3.0: the stream-json
6
- engine and the no-patch toolUse round-trip that replaced `pi.invokeTool`).
7
- Ideas that were weighed and rejected are listed at the end under "Discarded
8
- ideas".
9
-
10
- ## What the bridge already does
11
-
12
- agy -> bridge MCP server `tools/call` -> the call parks in the provider's
13
- round-trip store -> the provider ends the pi assistant message with a
14
- `toolUse` stop reason for the real pi tool -> pi executes it in its own loop
15
- (native cards, permissions, hooks) -> the `toolResult` completes the parked
16
- MCP response on the next stream call. No pi patch. Verified end-to-end with
17
- `memory_search` and `ask_user_question`. The bridge exposes pi's extension
18
- tools (builtins are filtered out since agy has native equivalents;
19
- `AskAntigravity` is filtered to avoid recursion).
20
-
21
- What this means in practice: agy can read/write files, use memory, navigate
22
- code with codegraph, search the web, post to Slack, create Asana tasks, spawn
23
- subagents, prompt the user with `ask_user_question`, and delegate to peer
24
- reviewers (Claude, Codex, Antigravity), all by going through pi's installed
25
- tooling instead of its own. Tools run in pi's process with pi's own
26
- credentials, so a secret never crosses the bridge, and a long call renders in
27
- pi's native UI while it runs. agy's file edits surface as git-sourced diffs in
28
- pi's thinking stream. A delta digest of pi-side context (compaction summaries,
29
- other-provider turns) is available but OFF by default (`/agy digest on`): the
30
- digest changes every turn and defeats agy's server-side prompt cache.
3
+ Open capability gaps only — things the bridge cannot do today, each blocked
4
+ on something outside this repo. For how the bridge works (engines, G9
5
+ round-trip, digest), see [ARCHITECTURE.md](./ARCHITECTURE.md). For shipped
6
+ work, see `CHANGELOG.md`. Historical gap labels (G1 digest, G8 edit diffs,
7
+ G9 round-trip, G10 system prompt) live in the CHANGELOG and source comments;
8
+ they are closed and not used here.
31
9
 
32
10
  ## Open gaps
33
11
 
34
- Two gaps remain. Both now sit on the no-patch round-trip path, so closing them
35
- means provider- or bridge-side work only; there is no pi dist patch to extend
36
- anymore. Ordered by impact.
12
+ ### pi UI primitives
37
13
 
38
- **Numbering note:** the G1/G2 labels below are this living doc's renumbered
39
- open set, NOT the historical G-numbers. In the historical list (`CHANGELOG.md`
40
- and the comments in `src/provider.ts`) G1 = conversation-history digest
41
- (shipped 1.0.0, now opt-in via `config.digest`) and G9 = the no-patch toolUse
42
- round-trip (shipped 1.3.0).
14
+ **Status:** Open. **Blocked by:** pi exposing a public API surface for these
15
+ without a patch (the old plan of patching `AgentSession.ui` into pi's dist is
16
+ dead; the bridge no longer patches pi).
43
17
 
44
- ---
45
-
46
- ### G1. Expose pi's UI primitives [MEDIUM-HIGH IMPACT]
47
-
48
- **Status:** Open
49
18
  **Objective:** Let agy drive pi's native UI: confirm dialogs, toasts,
50
- file/directory pickers, status/footer updates.
51
-
52
- **Why:** agy can already `ask_user_question`. Missing: confirm/permission
53
- dialog for destructive ops (agy falls back to its own out-of-theme dialog),
54
- notification toast (for "task started" / "save ok"), native file picker
55
- (replaced today by asking for a path in text), and status-bar updates
56
- ("Antigravity: working on X"). Note: this does NOT unlock a native diff viewer
57
- for agy edits, that path is structurally closed (see G8 in `CHANGELOG.md`).
19
+ file/directory pickers, status/footer updates. agy can already
20
+ `ask_user_question`; missing are confirm/permission dialogs for destructive
21
+ ops, notification toasts, native file pickers, and status-bar updates. This
22
+ does NOT unlock a native diff viewer for agy edits — on stream-json that path
23
+ is closed (G8 renders diffs as thinking text); on ACP the server supplies
24
+ edit diffs in `tool_call content[]`, rendered the same way.
58
25
 
59
- **Scope:**
60
- - pi-side: confirm a public API surface for these primitives. The old plan of
61
- patching `AgentSession.ui` into pi's dist is dead; the bridge no longer
62
- patches pi.
63
- - `src/mcp-server.ts`: wrappers for `pi_confirm`, `pi_notify`,
64
- `pi_select_file`, `pi_select_directory`, `pi_set_status`.
26
+ **Scope when unblocked:** wrappers in `src/mcp-server.ts`
27
+ (`pi_confirm`, `pi_notify`, `pi_select_file`, `pi_select_directory`,
28
+ `pi_set_status`), each parking through the provider round-trip; tests with a
29
+ mocked `ui` seam per primitive.
65
30
 
66
- **Acceptance criteria:**
67
- - [ ] `pi_confirm(message)` pops pi's native confirm UI and returns boolean.
68
- - [ ] `pi_notify(message)` shows a toast.
69
- - [ ] `pi_select_file`/`pi_select_directory` return chosen paths or null.
70
- - [ ] `pi_set_status(text)` updates the footer; clears on empty string.
71
- - [ ] Tests cover each primitive with a mocked `ui` seam.
31
+ ### Lifecycle event subscription
72
32
 
73
- **Effort:** Medium-large, gated on pi exposing the primitives without a patch.
33
+ **Status:** Open. **Blocked by:** a confirmed real consumer.
74
34
 
75
- **Blocks:** None. **Blocked by:** pi-side API availability.
76
-
77
- ---
78
-
79
- ### G2. Lifecycle event subscription [MEDIUM IMPACT]
80
-
81
- **Status:** Open
82
35
  **Objective:** Let a long-lived agy session observe pi events: `turn_start`,
83
- `turn_end`, `tool_call`, `tool_result`, `compaction`.
84
-
85
- **Why:** Today the bridge handles only `session_start` and `session_shutdown`.
86
- Event subscription would enable a class of "observer" tooling.
87
-
88
- **Caveat:** agy is still request-response per turn, even though the
89
- stream-json engine keeps one process alive across turns. Before building,
90
- confirm there is a real consumer that can act on an async event stream;
91
- otherwise this risks the same "no consumer" failure that sank file-watching
92
- (see Discarded ideas).
93
-
94
- **Scope:**
95
- - Provider-side event tap (no pi patch): relay pi event callbacks into the
96
- bridge.
97
- - `src/mcp-server.ts`: `pi_subscribe(event)` returns a stream id; an SSE
98
- channel pushes events.
36
+ `turn_end`, `tool_call`, `tool_result`, `compaction`. Today the bridge
37
+ handles only `session_start` and `session_shutdown`. Scope would be a
38
+ provider-side event tap plus a `pi_subscribe(event)` bridge tool with an SSE
39
+ channel.
99
40
 
100
- **Acceptance criteria:**
101
- - [ ] `pi_subscribe("tool_call")` returns a stream id and subsequent tool calls
102
- arrive on the channel.
103
- - [ ] Unsubscribe cleans up the stream (no leak).
104
- - [ ] At least three event types supported at close.
105
- - [ ] No perf regression on the event hot path.
41
+ **Caveat:** agy is request-response per turn even when the engine keeps a
42
+ process alive. Before building, confirm a consumer that can act on an async
43
+ event stream; otherwise this risks the same "no consumer" failure that sank
44
+ file-watching (see graveyard).
106
45
 
107
- **Effort:** Large.
46
+ ## Discarded ideas (graveyard)
108
47
 
109
- **Blocks:** None. **Blocked by:** Confirm a real event-driven consumer exists.
110
-
111
- ---
112
-
113
- ## Closed by 1.3.0 (moved out of the open set)
114
-
115
- - **Stream progress for long tool calls** (the former top open gap): closed by
116
- the no-patch round-trip. Bridged tools no longer block inside the MCP
117
- server; they execute as real pi tools in pi's own loop, so pi's native
118
- card/spinner UX shows progress while the call runs, and the result content
119
- is identical to the old blocking path.
120
-
121
- ## Discarded ideas (not worth it)
122
-
123
- Weighed and rejected; kept here as a graveyard so they are not re-proposed. Full
124
- reasoning is in project memory.
48
+ Weighed and rejected; kept so they are not re-proposed.
125
49
 
126
50
  - **Expose pi's other MCP clients** — REMOVED. pi has no native
127
51
  MCP-client support and no MCP extension is in use, so there are no pi
@@ -134,36 +58,19 @@ reasoning is in project memory.
134
58
  client that caches the list, and there is none.
135
59
  - **Settings, env, and secrets access** — DECLINED. Tools exposed via
136
60
  the bridge run in pi's process and self-authenticate with pi's own
137
- credentials, so agy already uses pi's creds for every tool; a credential
138
- never crosses the bridge. A `pi_get_setting` accessor was predicated on
139
- credential reuse that does not apply.
140
- - **Image / binary content blocks** — NOT NEEDED. pi shares the
141
- path to any image it produces (e.g. `/tmp/pi-clipboard-<uuid>.png`), and agy
61
+ credentials, so agy already uses pi's creds for every tool; a
62
+ credential never crosses the bridge. A `pi_get_setting` accessor was
63
+ predicated on credential reuse that does not apply.
64
+ - **Image / binary content blocks over the bridge** — NOT NEEDED. pi shares
65
+ the path to any image it produces (e.g. `/tmp/pi-clipboard-<uuid>.png`), and agy
142
66
  reaches and reads those files directly via the bridge's `read` tool, so
143
67
  returning image content blocks over the transport would duplicate a path
144
68
  that already works end-to-end. No agy transport change or pi patch required.
69
+ Update (2026-09-04): user-provided image *attachments* now ride natively on
70
+ the ACP engine as typed prompt content blocks (see README, Two
71
+ engines); the stream-json CLI prompt stays text-only, and the bridge
72
+ direction above is unchanged.
145
73
  - **File-watching / live state** — DECLINED. agy is request-response
146
74
  per turn, not event-reactive; nothing consumes a file-watch SSE stream, and
147
75
  re-reads are cheap and correct. Watchers would add inotify/FSEvents handles,
148
76
  races, and cleanup for no gain.
149
-
150
- ## How to close a gap
151
-
152
- For each open gap, the default shape:
153
-
154
- 1. Identify the pi-side API (must be public; the bridge does not patch pi).
155
- 2. Add a bridge tool wrapper in `src/mcp-server.ts` that parks a call through
156
- the provider's round-trip store, or answers bridge-side when no pi tool is
157
- needed (see `src/skills.ts` for that pattern).
158
- 3. Register the tool name with the bridge (it appears in agy's tool catalog on
159
- the next turn).
160
- 4. Add a test under `tests/mcp-server.test.ts` that round-trips a real call.
161
- 5. Tick the gap's acceptance checkboxes.
162
-
163
- Most need no change to agy, only the bridge or the provider.
164
-
165
- ## Cross-references
166
-
167
- - `docs/ARCHITECTURE.md` — engine internals, round-trip design, per-pid config layout.
168
- - `docs/DEVELOPMENT.md` — how to run tests, rebuild, and iterate.
169
- - `CHANGELOG.md` — shipped work.
@@ -37,9 +37,12 @@ import {
37
37
  import { SessionStore } from "../src/sessions.js";
38
38
  import { ToolRoundTrips, WrapperReplay, createStreamSimple } from "../src/provider.js";
39
39
  import { AgyDriver } from "../src/driver.js";
40
- import { CONFIG_PATH, loadConfig, saveConfig, type AgyMode, type BridgeTools, type ThinkingTier } from "../src/config.js";
40
+ import { AcpDriver } from "../src/acp/driver.js";
41
+ import { resolveAcpBinary } from "../src/acp/connection.js";
42
+ import type { TurnDriver } from "../src/driver-types.js";
43
+ import { CONFIG_PATH, loadConfig, saveConfig, type AgyMode, type BridgeTools, type Engine, type ThinkingTier } from "../src/config.js";
41
44
  import { registerAskAntigravityTool, toolModelsFromRaw } from "../src/ask-tool.js";
42
- import { startMcpServer, type McpServerHandle } from "../src/mcp-server.js";
45
+ import { startMcpServer, TOKEN_HEADER, type McpServerHandle } from "../src/mcp-server.js";
43
46
  import {
44
47
  ACTIVATE_SKILL_TOOL_NAME,
45
48
  activateSkillSchema,
@@ -79,14 +82,54 @@ export default async function (pi: ExtensionAPI): Promise<void> {
79
82
  const toolModels = toolModelsFromRaw(raw);
80
83
  const usingFallback = discovered.length === 0;
81
84
  const entries: AgyModelEntry[] = usingFallback ? FALLBACK_MODELS : discovered;
82
- const models = entries.map(toPiModel);
85
+ // Engine latched at load: /agy engine takes effect on the next pi start
86
+ // (documented). Everything below resolves from THIS value - per-call
87
+ // config reads would let a mid-session flip leave ToolRoundTrips,
88
+ // kickIdle, and reentry pointing at the other engine (round-7 finding).
89
+ const engine: Engine = loadConfig().engine;
90
+ // Engine switching requires a restart, so the catalog-time engine read is
91
+ // authoritative for input advertising: image attach rides only when turns
92
+ // will run on the ACP engine (the legacy CLI prompt is text-only).
93
+ const modelInput: Array<"text" | "image"> = engine === "acp" ? ["text", "image"] : ["text"];
94
+ const models = entries.map((e) => toPiModel(e, modelInput));
83
95
 
84
96
  const store = new SessionStore();
85
- // Persistent stream-json engine + the no-patch pi-tool round-trip store.
86
- // The MCP bridge parks calls here; the provider emits them as real pi
87
- // toolUse turns and completes them from the next call's toolResult.
88
- const driver = new AgyDriver();
89
- const roundTrips = new ToolRoundTrips(driver);
97
+ // MCP bridge handle, declared early: the ACP engine reads the bridge port
98
+ // at session/new / session/load time.
99
+ let mcpHandle: McpServerHandle | null = null;
100
+ // Two turn engines behind one contract (plan §9): stream-json (tested
101
+ // default) and the official ACP server (opt-in via config.engine, off by
102
+ // default). Neither spawns anything until its first turn.
103
+ const legacyDriver = new AgyDriver();
104
+ const acpDriver = new AcpDriver({
105
+ bin: resolveAcpBinary(loadConfig().acp.bin),
106
+ log: (msg, data) =>
107
+ console.error(`[antigravity-bridge acp] ${msg}${data !== undefined ? " " + JSON.stringify(data) : ""}`),
108
+ mcpServers: () => {
109
+ const handle = mcpHandle;
110
+ if (!handle) return [];
111
+ // The bridge 403s any request without the shared-secret header; the
112
+ // legacy engine carries it via mcp_config.json, ACP via headers[].
113
+ return [
114
+ {
115
+ name: "pi-bridge",
116
+ type: "http",
117
+ url: `http://127.0.0.1:${handle.port}/mcp`,
118
+ headers: [{ name: TOKEN_HEADER, value: handle.token }],
119
+ },
120
+ ];
121
+ },
122
+ });
123
+ // The active engine is resolved from the latched load-time value.
124
+ const activeDriver = (): TurnDriver => (engine === "acp" ? acpDriver : legacyDriver);
125
+ // The provider's stream-json slot gets the LEGACY driver explicitly - never
126
+ // activeDriver(), or a load-time acp engine would make deps.driver and
127
+ // deps.acpDriver the same object and break the engine identity check.
128
+ const driver = legacyDriver;
129
+ // The no-patch pi-tool round-trip store: the MCP bridge parks calls here;
130
+ // the provider emits them as real pi toolUse turns and completes them from
131
+ // the next call's toolResult.
132
+ const roundTrips = new ToolRoundTrips(activeDriver);
90
133
  const replay = new WrapperReplay();
91
134
  // Native re-exec only emits for builtins actually active in the session;
92
135
  // anything else (or an unknown name) falls back to the wrapper card.
@@ -99,9 +142,20 @@ export default async function (pi: ExtensionAPI): Promise<void> {
99
142
  }
100
143
  };
101
144
  // A settled turn cannot answer its parked calls; the driver never sees
102
- // ToolRoundTrips, so the provider bridges the two here.
103
- driver.onTurnEnd = () => roundTrips.failAll("antigravity turn ended with an unresolved pi tool call");
104
- const streamSimple = createStreamSimple({ entries, store, driver, roundTrips, replay, nativeActive });
145
+ // ToolRoundTrips, so the provider bridges the two here (both engines).
146
+ const onTurnEnd = () => roundTrips.failAll("antigravity turn ended with an unresolved pi tool call");
147
+ legacyDriver.onTurnEnd = onTurnEnd;
148
+ acpDriver.onTurnEnd = onTurnEnd;
149
+ const streamSimple = createStreamSimple({
150
+ entries,
151
+ store,
152
+ driver,
153
+ acpDriver,
154
+ roundTrips,
155
+ replay,
156
+ nativeActive,
157
+ engine,
158
+ });
105
159
 
106
160
  pi.registerProvider("antigravity", {
107
161
  name: "Antigravity (agy)",
@@ -122,7 +176,15 @@ export default async function (pi: ExtensionAPI): Promise<void> {
122
176
  streamSimple,
123
177
  });
124
178
 
125
- registerAgyCommand(pi, { entries, store, usingFallback, driver, getMcpPort: () => mcpHandle?.port ?? null });
179
+ registerAgyCommand(pi, {
180
+ entries,
181
+ store,
182
+ usingFallback,
183
+ driver,
184
+ acpDriver,
185
+ engine,
186
+ getMcpPort: () => mcpHandle?.port ?? null,
187
+ });
126
188
 
127
189
  // AskAntigravity tool: one-shot delegation to agy (ported from
128
190
  // pi-ask-antigravity). When both extensions are installed, the bridge wins
@@ -153,7 +215,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
153
215
  // Calls park in the provider's round-trip store and complete through pi's
154
216
  // normal toolUse loop (native cards, permissions, hooks) - no patch, no
155
217
  // privileged API. Started on session_start, torn down on session_shutdown.
156
- let mcpHandle: McpServerHandle | null = null;
157
218
  pi.on("session_start", async (_event, ctx) => {
158
219
  // Legacy cleanup: users who ran the old consent-gated patcher still
159
220
  // carry pi.invokeTool in their installed pi. Inert, but tell them once
@@ -266,7 +327,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
266
327
  mcpHandle = null;
267
328
  await h?.close();
268
329
  roundTrips.failAll("antigravity session shut down");
269
- await driver.close("shutdown");
330
+ await legacyDriver.close("shutdown");
331
+ await acpDriver.close("shutdown");
270
332
  });
271
333
  }
272
334
 
@@ -276,7 +338,10 @@ interface AgyCommandCtx {
276
338
  entries: AgyModelEntry[];
277
339
  store: SessionStore;
278
340
  usingFallback: boolean;
279
- driver: AgyDriver;
341
+ driver: TurnDriver;
342
+ acpDriver: AcpDriver;
343
+ /** Engine latched at extension load (see the provider wiring note). */
344
+ engine: Engine;
280
345
  getMcpPort: () => number | null;
281
346
  }
282
347
 
@@ -293,6 +358,7 @@ function statusText(ctx: AgyCommandCtx): string {
293
358
  const perm = config.skipPermissions ? "auto-approved (DANGEROUS)" : "prompt (hangs in -p)";
294
359
  return [
295
360
  "Antigravity bridge",
361
+ ` engine: ${config.engine}${config.engine === "acp" ? " (official server, opt-in)" : ""}`,
296
362
  ` models: ${ctx.entries.length} ${source}`,
297
363
  ` mode: ${config.mode}`,
298
364
  ` permissions: ${perm}`,
@@ -302,8 +368,9 @@ function statusText(ctx: AgyCommandCtx): string {
302
368
  ` config: ${CONFIG_PATH}`,
303
369
  ` bridge tools: ${config.bridgeTools}`,
304
370
  ` digest: ${config.digest ? "on" : "off"}`,
371
+ ` system prompt: ${config.systemPrompt ? "on" : "off"}`,
305
372
  "",
306
- "Subcommands: /agy mode plan|accept-edits, /agy permissions on|off, /agy model flash|pro|gemini, /agy thinking low|medium|high, /agy clear",
373
+ "Subcommands: /agy engine stream-json|acp, /agy mode plan|accept-edits, /agy permissions on|off, /agy model flash|pro|gemini, /agy thinking low|medium|high, /agy digest on|off, /agy system-prompt on|off, /agy acp-auth, /agy patch-cleanup, /agy clear",
307
374
  ].join("\n");
308
375
  }
309
376
 
@@ -311,7 +378,7 @@ function statusText(ctx: AgyCommandCtx): string {
311
378
  function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
312
379
  pi.registerCommand("agy", {
313
380
  description:
314
- "Antigravity provider: status, doctor, mode picker, clear sessions. Usage: /agy [status|doctor|mode [plan|accept-edits]|digest on|off|patch-cleanup|clear]",
381
+ "Antigravity provider: status, doctor, engine picker, mode picker, clear sessions. Usage: /agy [status|doctor|engine stream-json|acp|mode [plan|accept-edits]|permissions on|off|digest on|off|system-prompt on|off|acp-auth|patch-cleanup|clear]",
315
382
  handler: async (args, cmdCtx: ExtensionCommandContext) => {
316
383
  const ui = cmdCtx.ui;
317
384
  const mode = cmdCtx.mode;
@@ -344,19 +411,60 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
344
411
  );
345
412
  return;
346
413
  }
414
+ if (sub === "engine") {
415
+ if (val === "acp" || val === "stream-json") {
416
+ const next = saveConfig({ engine: val });
417
+ ui?.notify(
418
+ `engine set to ${next.engine}. Takes effect on the next pi start (or /reload).${next.engine === "acp" ? "\nRequires the official ACP server binary (AGY_ACP_BIN or acp.bin) and a one-time auth: /agy acp-auth" : ""}`,
419
+ "info",
420
+ );
421
+ } else {
422
+ ui?.notify(`current engine: ${loadConfig().engine}\nusage: /agy engine stream-json|acp`, "info");
423
+ }
424
+ return;
425
+ }
426
+ if (sub === "acp-auth") {
427
+ ui?.notify(
428
+ [
429
+ "ACP engine authentication (one-time):",
430
+ "1. Install the server binary (agy_acp_server.par from the ACP registry",
431
+ " build URL) and point acp.bin or AGY_ACP_BIN at it.",
432
+ "2. Pick one credential path:",
433
+ ' - oauth-personal: put {"auth":{"type":"oauth-personal"}} in',
434
+ " ~/.gemini/antigravity-acp/settings.json, run one turn, and open the",
435
+ " login URL the server produces (headless: tunnel 127.0.0.1:<port>",
436
+ " over ssh, then open the URL on your machine).",
437
+ ' - gemini-api-key: export GEMINI_API_KEY and set',
438
+ ' {"auth":{"type":"gemini-api-key"}} (headless-friendly; metered).',
439
+ " The agent never reads or writes your credentials.",
440
+ "3. Run one turn; /agy doctor shows the server version when auth is OK.",
441
+ ].join("\n"),
442
+ "info",
443
+ );
444
+ return;
445
+ }
347
446
  if (sub === "doctor") {
348
447
  const config = loadConfig();
349
- const snap = ctx.driver.snapshot();
448
+ const engine = ctx.engine;
449
+ const snap = (engine === "acp" ? ctx.acpDriver : ctx.driver).snapshot();
350
450
  const port = ctx.getMcpPort();
351
451
  const lines = [
352
452
  "Antigravity doctor (no tokens spent)",
453
+ ` engine: ${engine}`,
353
454
  ` bridge: ${config.bridgeTools}${port ? ` (port ${port})` : " (not running)"}`,
354
- ` driver: ${snap.state}${snap.pid ? ` pid=${snap.pid}` : ""}${snap.conversationId ? ` conv=${snap.conversationId.slice(0, 8)}` : ""}`,
455
+ ` driver: ${snap.state}${snap.pid ? ` pid=${snap.pid}` : ""}${snap.conversationId ? ` session=${snap.conversationId.slice(0, 8)}` : ""}`,
355
456
  ` driver stats: spawns=${snap.stats.spawns} turns=${snap.stats.turns} reused=${snap.stats.reused} recycles=${snap.stats.recycles}${snap.stats.lastRecycleReason ? ` (last: ${snap.stats.lastRecycleReason})` : ""}`,
356
457
  ` sessions: ${ctx.store.size} bound`,
357
458
  ` models: ${ctx.entries.length} ${ctx.usingFallback ? "FALLBACK (agy models failed)" : "discovered"}`,
358
459
  ` config: ${CONFIG_PATH}`,
359
460
  ];
461
+ if (snap.engine === "acp" && snap.acp) {
462
+ lines.push(
463
+ ` acp session: ${snap.acp.sessionId ?? "(none)"}`,
464
+ ` acp server: ${snap.acp.serverVersion ?? "unknown"}${snap.acp.agentTitle ? ` (${snap.acp.agentTitle})` : ""}`,
465
+ ` acp stats: prompts=${snap.acp.prompts} created=${snap.acp.sessionsCreated} loaded=${snap.acp.sessionsLoaded} kills=${snap.acp.kills} reconnects=${snap.acp.reconnects} cancel=${snap.acp.cancelSupported === null ? "unprobed" : snap.acp.cancelSupported ? "supported" : "unsupported (kill+reload)"}`,
466
+ );
467
+ }
360
468
  if (snap.lifecycle.length > 0) {
361
469
  lines.push(" lifecycle (last 5):");
362
470
  for (const entry of snap.lifecycle.slice(-5)) lines.push(` ${entry}`);
@@ -406,6 +514,20 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
406
514
  }
407
515
  return;
408
516
  }
517
+ if (sub === "system-prompt") {
518
+ if (val === "on" || val === "off") {
519
+ const next = saveConfig({ systemPrompt: val === "on" });
520
+ ui?.notify(
521
+ next.systemPrompt
522
+ ? "system-prompt on. pi's system prompt (incl. global and project AGENTS.md) is prepended to the first prompt of each new agy conversation. Existing conversations keep the version they started with."
523
+ : "system-prompt off. agy runs on its own system prompt; pi instructions and AGENTS.md files are not sent.",
524
+ "info",
525
+ );
526
+ } else {
527
+ ui?.notify(`system-prompt: ${loadConfig().systemPrompt ? "on" : "off"}\nusage: /agy system-prompt on|off`, "info");
528
+ }
529
+ return;
530
+ }
409
531
 
410
532
  if (sub === "thinking") {
411
533
  if (val === "low" || val === "medium" || val === "high") {
package/package.json CHANGED
@@ -1,14 +1,20 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-antigravity-bridge",
3
- "version": "1.3.2",
4
- "description": "Streaming Gemini provider for pi, built on the agy CLI. Registers antigravity/* models in pi's /model picker; drives agy through its stream-json protocol (persistent process, tool round-trips, live usage).",
3
+ "version": "1.4.0",
4
+ "description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. antigravity/* models in Pi's /model picker, no-patch MCP bridge: agy runs Pi's tools. ToS safe to use.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi-extension",
8
8
  "antigravity",
9
+ "antigravity-acp",
9
10
  "agy",
10
11
  "gemini",
11
12
  "google",
13
+ "google-acp",
14
+ "acp",
15
+ "agent-client-protocol",
16
+ "mcp",
17
+ "mcp-server",
12
18
  "provider",
13
19
  "streaming"
14
20
  ],