@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.
- package/CHANGELOG.md +43 -0
- package/README.md +22 -3
- package/docs/ACP-ADOPTION-PLAN.md +875 -0
- package/docs/ACP-PROTOCOL-REFERENCE.md +460 -0
- package/docs/ANTIGRAVITY-INTEGRATIONS.md +50 -551
- package/docs/ARCHITECTURE.md +50 -3
- package/docs/DEVELOPMENT.md +33 -0
- package/docs/PI-BRIDGE-GAPS.md +41 -140
- package/extensions/index.ts +126 -19
- package/package.json +8 -2
- package/src/acp/connection.ts +395 -0
- package/src/acp/driver.ts +719 -0
- package/src/acp/events.ts +250 -0
- package/src/acp/jsonrpc.ts +185 -0
- package/src/config.ts +33 -0
- package/src/diff-render.ts +15 -0
- package/src/driver-types.ts +144 -0
- package/src/driver.ts +49 -77
- package/src/mcp-server.ts +8 -1
- package/src/models.ts +9 -7
- package/src/provider.ts +140 -23
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
|
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).
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -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
|
|
package/docs/PI-BRIDGE-GAPS.md
CHANGED
|
@@ -1,133 +1,51 @@
|
|
|
1
1
|
# pi-antigravity-bridge: capability gaps
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
-
|
|
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
|
-
|
|
46
|
+
## Discarded ideas (graveyard)
|
|
114
47
|
|
|
115
|
-
|
|
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
|
|
144
|
-
never crosses the bridge. A `pi_get_setting` accessor was
|
|
145
|
-
credential reuse that does not apply.
|
|
146
|
-
- **Image / binary content blocks** — NOT NEEDED. pi shares
|
|
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.
|
package/extensions/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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, {
|
|
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
|
|
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:
|
|
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
|
|
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 ? `
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
],
|