@estebanforge/pi-antigravity-bridge 1.3.3 → 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,133 +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. The
31
- reverse direction is on by default (`/agy system-prompt off` to disable):
32
- pi's composed system prompt - its operating instructions plus the global
33
- agent-dir `AGENTS.md` and ancestor `AGENTS.md`/`CLAUDE.md` files - is
34
- prepended as a delimited block to the first prompt of each new agy
35
- conversation, once, so the prompt cache keeps hitting (G10 in
36
- `CHANGELOG.md`).
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.
37
9
 
38
10
  ## Open gaps
39
11
 
40
- Two gaps remain. Both now sit on the no-patch round-trip path, so closing them
41
- means provider- or bridge-side work only; there is no pi dist patch to extend
42
- anymore. Ordered by impact.
12
+ ### pi UI primitives
43
13
 
44
- **Numbering note:** the G1/G2 labels below are this living doc's renumbered
45
- open set, NOT the historical G-numbers. In the historical list (`CHANGELOG.md`
46
- and the comments in `src/provider.ts`) G1 = conversation-history digest
47
- (shipped 1.0.0, now opt-in via `config.digest`) and G9 = the no-patch toolUse
48
- 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).
49
17
 
50
- ---
51
-
52
- ### G1. Expose pi's UI primitives [MEDIUM-HIGH IMPACT]
53
-
54
- **Status:** Open
55
18
  **Objective:** Let agy drive pi's native UI: confirm dialogs, toasts,
56
- file/directory pickers, status/footer updates.
57
-
58
- **Why:** agy can already `ask_user_question`. Missing: confirm/permission
59
- dialog for destructive ops (agy falls back to its own out-of-theme dialog),
60
- notification toast (for "task started" / "save ok"), native file picker
61
- (replaced today by asking for a path in text), and status-bar updates
62
- ("Antigravity: working on X"). Note: this does NOT unlock a native diff viewer
63
- 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.
64
25
 
65
- **Scope:**
66
- - pi-side: confirm a public API surface for these primitives. The old plan of
67
- patching `AgentSession.ui` into pi's dist is dead; the bridge no longer
68
- patches pi.
69
- - `src/mcp-server.ts`: wrappers for `pi_confirm`, `pi_notify`,
70
- `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.
71
30
 
72
- **Acceptance criteria:**
73
- - [ ] `pi_confirm(message)` pops pi's native confirm UI and returns boolean.
74
- - [ ] `pi_notify(message)` shows a toast.
75
- - [ ] `pi_select_file`/`pi_select_directory` return chosen paths or null.
76
- - [ ] `pi_set_status(text)` updates the footer; clears on empty string.
77
- - [ ] Tests cover each primitive with a mocked `ui` seam.
31
+ ### Lifecycle event subscription
78
32
 
79
- **Effort:** Medium-large, gated on pi exposing the primitives without a patch.
33
+ **Status:** Open. **Blocked by:** a confirmed real consumer.
80
34
 
81
- **Blocks:** None. **Blocked by:** pi-side API availability.
82
-
83
- ---
84
-
85
- ### G2. Lifecycle event subscription [MEDIUM IMPACT]
86
-
87
- **Status:** Open
88
35
  **Objective:** Let a long-lived agy session observe pi events: `turn_start`,
89
- `turn_end`, `tool_call`, `tool_result`, `compaction`.
90
-
91
- **Why:** Today the bridge handles only `session_start` and `session_shutdown`.
92
- Event subscription would enable a class of "observer" tooling.
93
-
94
- **Caveat:** agy is still request-response per turn, even though the
95
- stream-json engine keeps one process alive across turns. Before building,
96
- confirm there is a real consumer that can act on an async event stream;
97
- otherwise this risks the same "no consumer" failure that sank file-watching
98
- (see Discarded ideas).
99
-
100
- **Scope:**
101
- - Provider-side event tap (no pi patch): relay pi event callbacks into the
102
- bridge.
103
- - `src/mcp-server.ts`: `pi_subscribe(event)` returns a stream id; an SSE
104
- 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.
105
40
 
106
- **Acceptance criteria:**
107
- - [ ] `pi_subscribe("tool_call")` returns a stream id and subsequent tool calls
108
- arrive on the channel.
109
- - [ ] Unsubscribe cleans up the stream (no leak).
110
- - [ ] At least three event types supported at close.
111
- - [ ] 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).
112
45
 
113
- **Effort:** Large.
46
+ ## Discarded ideas (graveyard)
114
47
 
115
- **Blocks:** None. **Blocked by:** Confirm a real event-driven consumer exists.
116
-
117
- ---
118
-
119
- ## Closed by 1.3.0 (moved out of the open set)
120
-
121
- - **Stream progress for long tool calls** (the former top open gap): closed by
122
- the no-patch round-trip. Bridged tools no longer block inside the MCP
123
- server; they execute as real pi tools in pi's own loop, so pi's native
124
- card/spinner UX shows progress while the call runs, and the result content
125
- is identical to the old blocking path.
126
-
127
- ## Discarded ideas (not worth it)
128
-
129
- Weighed and rejected; kept here as a graveyard so they are not re-proposed. Full
130
- reasoning is in project memory.
48
+ Weighed and rejected; kept so they are not re-proposed.
131
49
 
132
50
  - **Expose pi's other MCP clients** — REMOVED. pi has no native
133
51
  MCP-client support and no MCP extension is in use, so there are no pi
@@ -140,36 +58,19 @@ reasoning is in project memory.
140
58
  client that caches the list, and there is none.
141
59
  - **Settings, env, and secrets access** — DECLINED. Tools exposed via
142
60
  the bridge run in pi's process and self-authenticate with pi's own
143
- credentials, so agy already uses pi's creds for every tool; a credential
144
- never crosses the bridge. A `pi_get_setting` accessor was predicated on
145
- credential reuse that does not apply.
146
- - **Image / binary content blocks** — NOT NEEDED. pi shares the
147
- 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
148
66
  reaches and reads those files directly via the bridge's `read` tool, so
149
67
  returning image content blocks over the transport would duplicate a path
150
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.
151
73
  - **File-watching / live state** — DECLINED. agy is request-response
152
74
  per turn, not event-reactive; nothing consumes a file-watch SSE stream, and
153
75
  re-reads are cheap and correct. Watchers would add inotify/FSEvents handles,
154
76
  races, and cleanup for no gain.
155
-
156
- ## How to close a gap
157
-
158
- For each open gap, the default shape:
159
-
160
- 1. Identify the pi-side API (must be public; the bridge does not patch pi).
161
- 2. Add a bridge tool wrapper in `src/mcp-server.ts` that parks a call through
162
- the provider's round-trip store, or answers bridge-side when no pi tool is
163
- needed (see `src/skills.ts` for that pattern).
164
- 3. Register the tool name with the bridge (it appears in agy's tool catalog on
165
- the next turn).
166
- 4. Add a test under `tests/mcp-server.test.ts` that round-trips a real call.
167
- 5. Tick the gap's acceptance checkboxes.
168
-
169
- Most need no change to agy, only the bridge or the provider.
170
-
171
- ## Cross-references
172
-
173
- - `docs/ARCHITECTURE.md` — engine internals, round-trip design, per-pid config layout.
174
- - `docs/DEVELOPMENT.md` — how to run tests, rebuild, and iterate.
175
- - `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}`,
@@ -304,7 +370,7 @@ function statusText(ctx: AgyCommandCtx): string {
304
370
  ` digest: ${config.digest ? "on" : "off"}`,
305
371
  ` system prompt: ${config.systemPrompt ? "on" : "off"}`,
306
372
  "",
307
- "Subcommands: /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 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",
308
374
  ].join("\n");
309
375
  }
310
376
 
@@ -312,7 +378,7 @@ function statusText(ctx: AgyCommandCtx): string {
312
378
  function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
313
379
  pi.registerCommand("agy", {
314
380
  description:
315
- "Antigravity provider: status, doctor, mode picker, clear sessions. Usage: /agy [status|doctor|mode [plan|accept-edits]|digest on|off|system-prompt 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]",
316
382
  handler: async (args, cmdCtx: ExtensionCommandContext) => {
317
383
  const ui = cmdCtx.ui;
318
384
  const mode = cmdCtx.mode;
@@ -345,19 +411,60 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
345
411
  );
346
412
  return;
347
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
+ }
348
446
  if (sub === "doctor") {
349
447
  const config = loadConfig();
350
- const snap = ctx.driver.snapshot();
448
+ const engine = ctx.engine;
449
+ const snap = (engine === "acp" ? ctx.acpDriver : ctx.driver).snapshot();
351
450
  const port = ctx.getMcpPort();
352
451
  const lines = [
353
452
  "Antigravity doctor (no tokens spent)",
453
+ ` engine: ${engine}`,
354
454
  ` bridge: ${config.bridgeTools}${port ? ` (port ${port})` : " (not running)"}`,
355
- ` 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)}` : ""}`,
356
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})` : ""}`,
357
457
  ` sessions: ${ctx.store.size} bound`,
358
458
  ` models: ${ctx.entries.length} ${ctx.usingFallback ? "FALLBACK (agy models failed)" : "discovered"}`,
359
459
  ` config: ${CONFIG_PATH}`,
360
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
+ }
361
468
  if (snap.lifecycle.length > 0) {
362
469
  lines.push(" lifecycle (last 5):");
363
470
  for (const entry of snap.lifecycle.slice(-5)) lines.push(` ${entry}`);
package/package.json CHANGED
@@ -1,14 +1,20 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-antigravity-bridge",
3
- "version": "1.3.3",
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
  ],