@estebanforge/pi-antigravity-bridge 1.3.1 → 1.3.2
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 +12 -0
- package/README.md +11 -15
- package/docs/ARCHITECTURE.md +9 -40
- package/docs/DEVELOPMENT.md +5 -20
- package/extensions/index.ts +2 -5
- package/package.json +1 -3
- package/src/config.ts +0 -13
- package/src/provider.ts +9 -181
- package/src/poller.ts +0 -202
- package/src/protobuf.ts +0 -184
- package/src/runner.ts +0 -390
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.3.2] - 2026-09-01
|
|
6
|
+
|
|
7
|
+
### Removed
|
|
8
|
+
|
|
9
|
+
- The `legacy-sqlite` fallback engine: `src/runner.ts`, `src/poller.ts`, `src/protobuf.ts`, the `run-agy` and `decode-db` scripts, and the `engine` config key / `AGY_ENGINE` env var. agy 1.1.18 changed step-row storage to a two-phase write (a placeholder row first, grown in place later); the polling engine read each row once as an empty placeholder and never re-read it, so turns completed with the full reply in the database and zero text in pi (issue #1). The engine decoded an undocumented storage format, so every agy storage change risked repeating that failure silently. The stream-json engine shares none of that code path; verified live against agy 1.1.18-era storage (1.1.23 installed). A stale `engine` value in an existing `config.json` is ignored.
|
|
10
|
+
Reported by @imatimba in #1. Thanks for the exact repro and the root-cause analysis; the report drove this removal.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- `scripts/test-provider.ts` wires the stream-json driver explicitly (it exercised the legacy path implicitly before).
|
|
15
|
+
- `tests/provider-streaming.test.ts` covers effort mapping against a fake driver. The legacy event-mapping tests died with the engine; stream-json event coverage lives in `tests/stream-roundtrip.test.ts`.
|
|
16
|
+
|
|
5
17
|
## [1.3.1] - 2026-08-31
|
|
6
18
|
|
|
7
19
|
### Changed
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ If you also have [`@estebanforge/pi-ask-antigravity`](https://github.com/Esteban
|
|
|
8
8
|
|
|
9
9
|
## What it does
|
|
10
10
|
|
|
11
|
-
You pick a Gemini model in pi's `/model` picker. pi routes each turn through this provider.
|
|
11
|
+
You pick a Gemini model in pi's `/model` picker. pi routes each turn through this provider. A single persistent `agy` process runs in your workspace; pi feeds it each turn, parses its stream-json events, and streams the agent text back into pi token by token. Token usage is live.
|
|
12
12
|
|
|
13
13
|
Multi-turn works. The provider binds a pi session to an agy conversation id (persisted under `~/.pi/agent/antigravity-bridge/sessions.json`) and resumes it on the next turn via `--conversation <id>`. agy keeps its own history, so only the latest user message is sent each turn.
|
|
14
14
|
|
|
@@ -20,7 +20,7 @@ Residual limits (with or without the bridge):
|
|
|
20
20
|
|
|
21
21
|
- agy's own edits still land directly on disk; pi's inline diff review does not engage for them.
|
|
22
22
|
- agy commands run without per-action approval, same as every other tool in pi. See [Permissions](#permissions) below.
|
|
23
|
-
- No cost accounting: cost stays zero because agy runs on your subscription quota. Token usage is live
|
|
23
|
+
- No cost accounting: cost stays zero because agy runs on your subscription quota. Token usage is live.
|
|
24
24
|
|
|
25
25
|
## MCP tool bridge (agy uses pi's tools)
|
|
26
26
|
|
|
@@ -36,7 +36,7 @@ The bridge starts a localhost MCP server inside pi's process. `tools/list` retur
|
|
|
36
36
|
|
|
37
37
|
**Security.** The MCP server binds to `127.0.0.1` only and requires a per-session shared-secret header (`x-bridge-token`) that agy sends from the bridge config; browsers cannot set custom headers on a simple cross-origin POST, so this blocks web CSRF against the loopback server. Request bodies are size-capped. This is intended for single-user developer machines: any local process running as the same user can read the token from the per-pid config and call the exposed tools, so do not run it on a shared host where you do not trust other same-user processes.
|
|
38
38
|
|
|
39
|
-
### Native cards, wrapper replay, and skills
|
|
39
|
+
### Native cards, wrapper replay, and skills
|
|
40
40
|
|
|
41
41
|
Read-only agy steps (view_file, list_dir, grep_search, find_by_name) re-run as
|
|
42
42
|
real pi builtins (`read`, `ls`, `grep`, `find`) when those builtins are active,
|
|
@@ -48,9 +48,8 @@ anything with side effects.
|
|
|
48
48
|
|
|
49
49
|
When the bridge is on, agy also gets one `activate_skill` tool whose enum is
|
|
50
50
|
your pi Agent Skills catalog; calling it returns the SKILL.md body. The bridge
|
|
51
|
-
answers it directly, no pi round-trip. `/agy doctor` prints
|
|
52
|
-
|
|
53
|
-
tokens.
|
|
51
|
+
answers it directly, no pi round-trip. `/agy doctor` prints driver counters,
|
|
52
|
+
bridge port, and the last lifecycle events without spending tokens.
|
|
54
53
|
|
|
55
54
|
## Install
|
|
56
55
|
|
|
@@ -64,8 +63,6 @@ pi install npm:@estebanforge/pi-antigravity-bridge
|
|
|
64
63
|
|
|
65
64
|
Requires the **`agy` CLI** installed and authenticated. If you don't have it, follow Google's [official install guide](https://antigravity.google/docs/cli/install) for your platform, then run `agy` once to complete Google OAuth. The extension resolves `agy` on `$PATH`, or via the `AGY_BIN` environment variable.
|
|
66
65
|
|
|
67
|
-
Also requires Node 22.5 or newer (uses the built-in `node:sqlite`).
|
|
68
|
-
|
|
69
66
|
## Usage
|
|
70
67
|
|
|
71
68
|
Pick a model and talk to pi as usual:
|
|
@@ -86,17 +83,16 @@ Model ids are slugified from the `agy models` output (`Gemini 3.6 Flash (Medium)
|
|
|
86
83
|
|
|
87
84
|
If `agy models` fails at load (binary missing, auth not done, network stall), a fallback catalog still populates the picker so you get a clear runtime error instead of an empty list.
|
|
88
85
|
|
|
89
|
-
###
|
|
86
|
+
### Bridge surface
|
|
90
87
|
|
|
91
|
-
`config.json` selects the
|
|
88
|
+
`config.json` selects the bridge surface:
|
|
92
89
|
|
|
93
90
|
| Key | Values | Default |
|
|
94
91
|
| --- | --- | --- |
|
|
95
|
-
| `engine` | `stream-json` (persistent agy process, toolUse round-trips, live usage) or `legacy-sqlite` (spawn `agy -p`, poll its SQLite) | `stream-json` |
|
|
96
92
|
| `bridgeTools` | `none` (bridge off), `mcp` (pi-mcp-adapter tools), `all` (every non-builtin tool, incl. other `Ask*` delegations) | `mcp` |
|
|
97
93
|
| `digest` | `off` (stable prompts; agy's prompt cache hits) or `on` (inject a delta of pi-side context - compaction summaries, other-provider turns - into each agy prompt; the delta changes every turn, so agy re-bills the full context). Enable for mixed-provider sessions where agy must see pi-side context | `off` |
|
|
98
94
|
|
|
99
|
-
Env overrides: `
|
|
95
|
+
Env overrides: `AGY_BRIDGE_TOOLS`, `AGY_DIGEST`.
|
|
100
96
|
|
|
101
97
|
### The /agy command
|
|
102
98
|
|
|
@@ -105,13 +101,13 @@ Env overrides: `AGY_ENGINE`, `AGY_BRIDGE_TOOLS`, `AGY_DIGEST`. The `legacy-sqlit
|
|
|
105
101
|
```
|
|
106
102
|
/agy status, or open the mode/permissions/model/thinking picker (TUI)
|
|
107
103
|
/agy status print current mode, permissions, model + session counts
|
|
108
|
-
/agy doctor
|
|
104
|
+
/agy doctor bridge state, driver counters, bridge port, last lifecycle events
|
|
109
105
|
/agy mode plan review-only: agy plans but writes nothing
|
|
110
106
|
/agy mode accept-edits agy applies edits directly (default)
|
|
111
107
|
/agy permissions on|off auto-approve / prompt for tool calls (see warning)
|
|
112
108
|
/agy model flash|pro|gemini default model alias for the AskAntigravity tool
|
|
113
109
|
/agy thinking low|medium|high default thinking tier for the AskAntigravity tool
|
|
114
|
-
/agy digest on|off inject pi-side context into agy prompts (default off; see
|
|
110
|
+
/agy digest on|off inject pi-side context into agy prompts (default off; see table above)
|
|
115
111
|
/agy patch-cleanup restore the original pi files if an older version patched them
|
|
116
112
|
/agy clear drop all session bindings (force fresh conversations)
|
|
117
113
|
```
|
|
@@ -148,7 +144,7 @@ Build, test, and debug instructions live in [docs/DEVELOPMENT.md](docs/DEVELOPME
|
|
|
148
144
|
|
|
149
145
|
Google's [Antigravity ToS](https://antigravity.google/terms) (Section 6) prohibits accessing the service "in connection with products not provided by us", and names as its example using tools like Hermes/OpenClaw with Antigravity OAuth. That targets reusing your credentials in a non-Google harness that calls Google's backend directly.
|
|
150
146
|
|
|
151
|
-
This extension does not do that. It spawns the official, unmodified `agy` binary as a subprocess; `agy` performs its own OAuth and makes its own calls to Google. This code never sees, extracts, or reuses your token, and never contacts Antigravity's backend. It only reads what `agy` itself produces locally: its stream-json output
|
|
147
|
+
This extension does not do that. It spawns the official, unmodified `agy` binary as a subprocess; `agy` performs its own OAuth and makes its own calls to Google. This code never sees, extracts, or reuses your token, and never contacts Antigravity's backend. It only reads what `agy` itself produces locally: its stream-json output. From Google's server-side view there is no signal that distinguishes "agy launched by pi" from "agy launched by a terminal, an IDE task runner, or cron": same signed binary, same authenticated calls.
|
|
152
148
|
|
|
153
149
|
Google's reported enforcement to date (the February 2026 suspensions) targeted token-reuse tools, not spawning the official CLI.
|
|
154
150
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
How the provider works internally. For build/test/debug workflow see [DEVELOPMENT.md](./DEVELOPMENT.md).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Turn engine
|
|
6
6
|
|
|
7
|
-
The provider runs one
|
|
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.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- **`legacy-sqlite` (fallback):** the pre-1.3.0 engine - spawn `agy -p`, poll its SQLite conversation DB on a 250 ms interval, decode the protobuf step payloads. Kept as a fallback and scheduled for removal once `stream-json` has burned in (`AGY_ENGINE=legacy-sqlite` selects it).
|
|
11
|
-
|
|
12
|
-
Shared by both engines: session binding (`sessions.json`), runtime config, the `AskAntigravity` tool, the MCP tool bridge surface, and the G1 context digest (off by default - see below).
|
|
9
|
+
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).
|
|
13
10
|
|
|
14
11
|
## Module map
|
|
15
12
|
|
|
@@ -21,21 +18,18 @@ src/stream-events.ts agy NDJSON event parser (init / step_update / result) + us
|
|
|
21
18
|
src/native-tools.ts maps agy read-only tool steps to real pi builtins (read/ls/grep/find) for native re-execution
|
|
22
19
|
src/skills.ts activate_skill bridge: exposes the pi Agent Skills catalog to agy, answered by the bridge directly
|
|
23
20
|
src/patch-cleanup.ts detects a leftover invokeTool patch from pre-1.3.0 installs; /agy patch-cleanup restores the backup
|
|
24
|
-
src/
|
|
25
|
-
src/poller.ts legacy engine: read-only node:sqlite handle over one conversation DB
|
|
26
|
-
src/protobuf.ts legacy engine: hand-rolled varint walker + extractors (field 20.1 = text)
|
|
27
|
-
src/discovery.ts legacy engine: snapshot/diff + pid fd-scan to bind the conversation id agy -p never prints
|
|
21
|
+
src/discovery.ts conversation-id binding for the AskAntigravity one-shot tool (agy -p never prints its conversation id)
|
|
28
22
|
src/models.ts agy models -> pi Model projection (full catalog, per-model effort)
|
|
29
23
|
src/sessions.ts atomic JSON store: pi session -> agy conversation + watermark
|
|
30
|
-
src/config.ts persisted runtime config (
|
|
24
|
+
src/config.ts persisted runtime config (bridgeTools, digest, mode, permissions, model/thinking defaults)
|
|
31
25
|
src/ask-tool.ts the AskAntigravity one-shot delegation tool (model/thinking defaults)
|
|
32
26
|
src/mcp-server.ts MCP tool bridge server: ferries tools/list + tools/call; calls park in the provider round-trip
|
|
33
27
|
src/diff-render.ts render agy's file edits as git diffs in pi's thinking stream
|
|
34
28
|
```
|
|
35
29
|
|
|
36
|
-
No generated protobuf code, no
|
|
30
|
+
No generated protobuf code, no SQLite dependency.
|
|
37
31
|
|
|
38
|
-
## Stream-json engine
|
|
32
|
+
## Stream-json engine
|
|
39
33
|
|
|
40
34
|
### Process and events
|
|
41
35
|
|
|
@@ -61,31 +55,6 @@ Display follows the same split: agy read-only steps (`view_file`, `list_dir`, `g
|
|
|
61
55
|
|
|
62
56
|
By default the prompt agy receives is only the latest user message: agy keeps its own history. When `config.digest` is on (`AGY_DIGEST`, `/agy digest on`), the provider prepends a DELTA digest of pi-side context agy was not spawned for - compaction summaries, other-provider turns, pi-tool results. Off by default because the digest changes every turn and defeats agy's server-side prompt cache (~25-30k tokens re-billed per turn). Enable it for mixed-provider sessions where agy must see pi-side context; pure antigravity sessions gain nothing, and bridge round-trips deliver tool results through the bridge, not the digest.
|
|
63
57
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
The sections below describe the `legacy-sqlite` fallback only (`AGY_ENGINE=legacy-sqlite`). The stream-json engine shares none of this code path.
|
|
67
|
-
|
|
68
|
-
### The decode pipeline
|
|
69
|
-
|
|
70
|
-
agy writes each step to a SQLite row with a protobuf blob in `step_payload`. The text we want lives at field 20, submessage field 1. Tool calls live at field 5, submessage field 4, with the name at field 2 or 9 and the raw input JSON at field 3. These field numbers are reverse-engineered facts (cross-checked against the shindgew/agy-acp and shubzkothekar/antigravity-acp decoders, then verified against real databases on agy 1.1.7). They are load-bearing. Unknown fields are skipped per protobuf wire rules, so a future agy that adds fields will not break decoding.
|
|
71
|
-
|
|
72
|
-
### Step types
|
|
73
|
-
|
|
74
|
-
| step_type | meaning |
|
|
75
|
-
| --- | --- |
|
|
76
|
-
| 15 | agent text (payload field 20 -> field 1) |
|
|
77
|
-
| 14 | thinking |
|
|
78
|
-
| 23 | title update (payload field 30 -> field 4) |
|
|
79
|
-
| 5, 7, 8, 9, 17, 21, 33, 101, 132, 138 | tool run (payload field 5 -> field 4 -> name@2/9, input@3) |
|
|
80
|
-
|
|
81
|
-
Status 3 = complete; anything else = in-flight.
|
|
82
|
-
|
|
83
|
-
### Polling
|
|
84
|
-
|
|
85
|
-
The runner spawns `agy -p`, then polls its conversation DB on a 250ms interval concurrent with the running process (this is what makes the provider actually stream, not replay at exit). Each tick issues a single `PRAGMA data_version` check; while agy is thinking and has not committed, that check is false and no row SELECT runs at all (neither the new-row read nor the in-place re-read of the step agy is currently extending). Only when a commit lands do both reads fire in one pass. Three trailing polls at 100ms after agy exits catch the last flush; on abort these are skipped so cancellation is prompt.
|
|
86
|
-
|
|
87
|
-
### Conversation id discovery
|
|
88
|
-
|
|
89
|
-
agy `-p` does not print the conversation id. On a fresh run the runner snapshots the `*.db` stems in the conversations dir before spawn, then diffs after. Exactly one new file = ours; zero new = refuse to bind (surfaced as an error rather than a guess).
|
|
58
|
+
### Removed: the legacy-sqlite engine (1.3.2)
|
|
90
59
|
|
|
91
|
-
|
|
60
|
+
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.
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -14,22 +14,9 @@ The integration scripts below spawn a real `agy` process and need a logged-in ac
|
|
|
14
14
|
|
|
15
15
|
## Standalone scripts
|
|
16
16
|
|
|
17
|
-
These exercise the pipeline without pi. Useful for isolating where a bug lives (
|
|
17
|
+
These exercise the pipeline without pi. Useful for isolating where a bug lives (driver? provider? pi loader?).
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
# Decode any conversation DB and print agent text + tool calls.
|
|
21
|
-
# Accepts a UUID (resolved against ~/.gemini/antigravity-cli/conversations/)
|
|
22
|
-
# or an absolute path. Fastest way to check the protobuf decoder against
|
|
23
|
-
# real data. No agy spawn, no network.
|
|
24
|
-
npm run decode-db -- <uuid-or-path>
|
|
25
|
-
|
|
26
|
-
# Spawn agy and stream decoded events to stdout with timestamps. Proves the
|
|
27
|
-
# concurrent poll loop actually streams (events arrive during the run, not
|
|
28
|
-
# only at exit). Use this to reproduce a hang or a missing-event bug.
|
|
29
|
-
npm run run-agy -- "Say hello"
|
|
30
|
-
npm run run-agy -- --model "Gemini 3.6 Flash (Medium)" --mode plan "Review src/protobuf.ts"
|
|
31
|
-
npm run run-agy -- --conversation <uuid> "follow up" # resume a turn
|
|
32
|
-
|
|
33
20
|
# Drive the provider's streamSimple directly (no pi TUI) and assert the
|
|
34
21
|
# full event lifecycle: start -> text_start -> text_delta -> text_end ->
|
|
35
22
|
# done. The closest thing to a pi turn without pi.
|
|
@@ -53,21 +40,19 @@ AGY_LIVE=1 node --experimental-strip-types scripts/smoke-stream-json.mjs
|
|
|
53
40
|
|
|
54
41
|
Most "stuck" reports trace to one of:
|
|
55
42
|
|
|
56
|
-
1. **agy blocked on a permission prompt.** `accept-edits` auto-approves file edits but NOT shell commands. Any `run_command` prompts `y/n`, which hangs forever in non-interactive mode
|
|
43
|
+
1. **agy blocked on a permission prompt.** `accept-edits` auto-approves file edits but NOT shell commands. Any `run_command` prompts `y/n`, which hangs forever in non-interactive mode. The provider passes `--dangerously-skip-permissions` by default to avoid this. If you turned it off (`/agy permissions off`), that is why. See the README Permissions section.
|
|
57
44
|
2. **agy never started.** Check `AGY_BIN` is on PATH (or set explicitly). The spawner swallows spawn ENOENT into the result's stderr, surfaced by the provider as an error event.
|
|
58
|
-
3. **Conversation id never bound.** On `stream-json` the `init` event carries the id
|
|
45
|
+
3. **Conversation id never bound.** On `stream-json` the `init` event carries the id, so a missing binding means the turn never produced a result event. `/agy doctor` prints the last lifecycle events.
|
|
59
46
|
4. **Print-mode environmental hang.** `pi -p` can hang with zero output in some containers (upstream [google-antigravity/antigravity-cli#318](https://github.com/google-antigravity/antigravity-cli/issues/318)). It affects built-in providers too, not this extension. Validate the turn with `scripts/test-provider.ts` instead.
|
|
60
47
|
|
|
61
48
|
## Regression tests worth knowing
|
|
62
49
|
|
|
63
50
|
- `tests/stream-roundtrip.test.ts` - the stream-json engine pieces: NDJSON parser, native re-exec mapping, and the no-patch toolUse round-trip store.
|
|
64
|
-
- `tests/provider-streaming.test.ts` - drives streamSimple with an injected fake
|
|
51
|
+
- `tests/provider-streaming.test.ts` - drives streamSimple with an injected fake driver (no agy) and asserts how pi's reasoning level maps onto the agy `--effort` tier (forward, clamp, omit).
|
|
65
52
|
- `tests/provider-digest.test.ts` - the G1 context digest builder: injects pi-side context without replaying agy's own history.
|
|
66
53
|
- `tests/patch-cleanup.test.ts` - legacy-patch detection and restore, real fs via tmpdirs, no mocks.
|
|
67
|
-
- `tests/runner-streaming.test.ts` - (legacy engine) a fake agy writes rows on a delay; asserts events arrive DURING the run (not all at exit) and that abort returns promptly. Guards the "provider did not actually stream" class of bug.
|
|
68
|
-
- `tests/protobuf.test.ts` - (legacy engine) pure decoder math (varint, field walking, nested submessages).
|
|
69
54
|
- `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.
|
|
70
55
|
|
|
71
56
|
## Module map
|
|
72
57
|
|
|
73
|
-
See [ARCHITECTURE.md](./ARCHITECTURE.md) for the module map and the engine internals (stream-json events, no-patch round-trip
|
|
58
|
+
See [ARCHITECTURE.md](./ARCHITECTURE.md) for the module map and the engine internals (stream-json events, no-patch round-trip).
|
package/extensions/index.ts
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Registers Gemini (via the agy CLI) as a pi model provider so it shows up in
|
|
4
4
|
// the /model picker as antigravity/gemini-*. When selected, pi routes each turn
|
|
5
|
-
// through streamSimple, which
|
|
6
|
-
//
|
|
7
|
-
// back into pi's TUI.
|
|
5
|
+
// through streamSimple, which feeds the persistent stream-json driver process
|
|
6
|
+
// and streams the agent text back into pi's TUI.
|
|
8
7
|
//
|
|
9
8
|
// Architectural wall (cannot be worked around - see PLAN.md):
|
|
10
9
|
// agy runs its OWN closed tool loop against --add-dir. pi's read/write/edit/
|
|
@@ -301,7 +300,6 @@ function statusText(ctx: AgyCommandCtx): string {
|
|
|
301
300
|
` tool thinking: ${config.defaultThinking}`,
|
|
302
301
|
` sessions: ${ctx.store.size} bound`,
|
|
303
302
|
` config: ${CONFIG_PATH}`,
|
|
304
|
-
` engine: ${config.engine}`,
|
|
305
303
|
` bridge tools: ${config.bridgeTools}`,
|
|
306
304
|
` digest: ${config.digest ? "on" : "off"}`,
|
|
307
305
|
"",
|
|
@@ -352,7 +350,6 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
352
350
|
const port = ctx.getMcpPort();
|
|
353
351
|
const lines = [
|
|
354
352
|
"Antigravity doctor (no tokens spent)",
|
|
355
|
-
` engine: ${config.engine}`,
|
|
356
353
|
` bridge: ${config.bridgeTools}${port ? ` (port ${port})` : " (not running)"}`,
|
|
357
354
|
` driver: ${snap.state}${snap.pid ? ` pid=${snap.pid}` : ""}${snap.conversationId ? ` conv=${snap.conversationId.slice(0, 8)}` : ""}`,
|
|
358
355
|
` 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})` : ""}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
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).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -30,8 +30,6 @@
|
|
|
30
30
|
"build": "tsc --noEmit",
|
|
31
31
|
"test": "vitest run",
|
|
32
32
|
"prepublishOnly": "npm run build && npm test",
|
|
33
|
-
"run-agy": "tsx scripts/run-agy.ts",
|
|
34
|
-
"decode-db": "tsx scripts/decode-db.ts",
|
|
35
33
|
"smoke:pi": "bash scripts/smoke-in-pi.sh"
|
|
36
34
|
},
|
|
37
35
|
"files": [
|
package/src/config.ts
CHANGED
|
@@ -24,7 +24,6 @@ const CONFIG_PATH = path.join(
|
|
|
24
24
|
|
|
25
25
|
export type AgyMode = "accept-edits" | "plan";
|
|
26
26
|
export type ThinkingTier = "low" | "medium" | "high";
|
|
27
|
-
export type AgyEngine = "stream-json" | "legacy-sqlite";
|
|
28
27
|
export type BridgeTools = "none" | "mcp" | "all";
|
|
29
28
|
|
|
30
29
|
export interface AgyConfig {
|
|
@@ -39,11 +38,6 @@ export interface AgyConfig {
|
|
|
39
38
|
defaultModel: string;
|
|
40
39
|
/** AskAntigravity tool: default thinking tier when the alias names none. */
|
|
41
40
|
defaultThinking: ThinkingTier;
|
|
42
|
-
/** Turn engine. "stream-json" (default): one persistent agy process fed
|
|
43
|
-
* NDJSON user events; enables live toolUse round-trips, native usage, and
|
|
44
|
-
* conversation binding from the init event. "legacy-sqlite": the old
|
|
45
|
-
* spawn-`agy -p`-and-poll-SQLite path, kept as a fallback for one release. */
|
|
46
|
-
engine: AgyEngine;
|
|
47
41
|
/** Set after the one-time notice about a leftover legacy invokeTool patch
|
|
48
42
|
* on the installed pi. The notice never repeats; /agy patch-cleanup is
|
|
49
43
|
* always available. */
|
|
@@ -73,7 +67,6 @@ const DEFAULTS: AgyConfig = {
|
|
|
73
67
|
skipPermissions: true,
|
|
74
68
|
defaultModel: "flash",
|
|
75
69
|
defaultThinking: "medium",
|
|
76
|
-
engine: "stream-json",
|
|
77
70
|
bridgeTools: "mcp",
|
|
78
71
|
digest: false,
|
|
79
72
|
};
|
|
@@ -120,11 +113,6 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
|
|
|
120
113
|
const defaultThinking: ThinkingTier =
|
|
121
114
|
thinkRaw === "low" || thinkRaw === "high" ? thinkRaw : "medium";
|
|
122
115
|
|
|
123
|
-
const engine: AgyEngine =
|
|
124
|
-
process.env.AGY_ENGINE === "legacy-sqlite" || file.engine === "legacy-sqlite"
|
|
125
|
-
? "legacy-sqlite"
|
|
126
|
-
: "stream-json";
|
|
127
|
-
|
|
128
116
|
const bridgeRaw = (process.env.AGY_BRIDGE_TOOLS ?? file.bridgeTools ?? DEFAULTS.bridgeTools).toLowerCase();
|
|
129
117
|
const bridgeTools: BridgeTools =
|
|
130
118
|
bridgeRaw === "none" || bridgeRaw === "all" ? bridgeRaw : "mcp";
|
|
@@ -138,7 +126,6 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
|
|
|
138
126
|
skipPermissions,
|
|
139
127
|
defaultModel,
|
|
140
128
|
defaultThinking,
|
|
141
|
-
engine,
|
|
142
129
|
bridgeTools,
|
|
143
130
|
digest,
|
|
144
131
|
patchCleanupNotified: file.patchCleanupNotified === true,
|
package/src/provider.ts
CHANGED
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
type Usage,
|
|
30
30
|
} from "@earendil-works/pi-ai";
|
|
31
31
|
import type { Api } from "@earendil-works/pi-ai";
|
|
32
|
-
import { runAgyTurn, type AgyEvent, type AgyRunOptions } from "./runner.js";
|
|
33
32
|
import { AgyDriver, type DriverActivity, type TurnHandle } from "./driver.js";
|
|
34
33
|
import { toPiUsage } from "./stream-events.js";
|
|
35
34
|
import { mapAgyToolToNative } from "./native-tools.js";
|
|
@@ -245,11 +244,8 @@ export interface BlockState {
|
|
|
245
244
|
export interface StreamSimpleDeps {
|
|
246
245
|
entries: AgyModelEntry[];
|
|
247
246
|
store: SessionStore;
|
|
248
|
-
/**
|
|
249
|
-
*
|
|
250
|
-
runAgyTurn?: typeof runAgyTurn;
|
|
251
|
-
/** Persistent stream-json engine. When set (and config.engine selects it),
|
|
252
|
-
* turns run on the driver and bridge calls park as toolUse round-trips. */
|
|
247
|
+
/** Persistent stream-json driver. Turns run on the driver and bridge
|
|
248
|
+
* calls park as toolUse round-trips. Required with roundTrips. */
|
|
253
249
|
driver?: AgyDriver;
|
|
254
250
|
roundTrips?: ToolRoundTrips;
|
|
255
251
|
/** Replay store for the display-only antigravity wrapper tool. Required
|
|
@@ -692,13 +688,12 @@ async function runTurnDriver(
|
|
|
692
688
|
export function createStreamSimple(
|
|
693
689
|
deps: StreamSimpleDeps,
|
|
694
690
|
): (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream {
|
|
695
|
-
const { entries, store,
|
|
691
|
+
const { entries, store, driver, roundTrips } = deps;
|
|
696
692
|
|
|
697
693
|
return function streamSimple(model, context, options) {
|
|
698
694
|
const stream = createAssistantMessageEventStream();
|
|
699
695
|
// Fire the async turn; return the stream synchronously per pi's contract.
|
|
700
|
-
|
|
701
|
-
if (driver && roundTrips && config.engine === "stream-json") {
|
|
696
|
+
if (driver && roundTrips) {
|
|
702
697
|
void runTurnDriver(stream, model, context, options, entries, store, {
|
|
703
698
|
driver,
|
|
704
699
|
roundTrips,
|
|
@@ -706,183 +701,16 @@ export function createStreamSimple(
|
|
|
706
701
|
nativeActive: deps.nativeActive,
|
|
707
702
|
});
|
|
708
703
|
} else {
|
|
709
|
-
|
|
704
|
+
// Miswired extension: no driver means no engine. Fail the turn visibly
|
|
705
|
+
// instead of silently producing an empty assistant message.
|
|
706
|
+
const partial = newAssistant(model);
|
|
707
|
+
const blocks: BlockState = { partial, textIdx: null, thinkingIdx: null, started: false };
|
|
708
|
+
finalize(stream, blocks, "error", "antigravity driver not configured");
|
|
710
709
|
}
|
|
711
710
|
return stream;
|
|
712
711
|
};
|
|
713
712
|
}
|
|
714
713
|
|
|
715
|
-
async function runTurn(
|
|
716
|
-
stream: AssistantMessageEventStream,
|
|
717
|
-
model: Model<Api>,
|
|
718
|
-
context: Context,
|
|
719
|
-
options: SimpleStreamOptions | undefined,
|
|
720
|
-
entries: AgyModelEntry[],
|
|
721
|
-
store: SessionStore,
|
|
722
|
-
runFn: typeof runAgyTurn,
|
|
723
|
-
): Promise<void> {
|
|
724
|
-
const partial = newAssistant(model);
|
|
725
|
-
const blocks: BlockState = { partial, textIdx: null, thinkingIdx: null, started: false };
|
|
726
|
-
|
|
727
|
-
// Direct emit helpers. agy streams deltas that may not align to line
|
|
728
|
-
// boundaries; pi's TUI renders partial lines fine, so we append and push
|
|
729
|
-
// each delta straight through (no filtering, no buffering).
|
|
730
|
-
const appendTextDelta = (delta: string): void => {
|
|
731
|
-
appendText(stream, blocks, delta);
|
|
732
|
-
};
|
|
733
|
-
const appendThinkingDelta = (delta: string): void => {
|
|
734
|
-
appendThinking(stream, blocks, delta);
|
|
735
|
-
};
|
|
736
|
-
|
|
737
|
-
// Signal the turn has begun IMMEDIATELY. pi's native Working indicator is
|
|
738
|
-
// driven by the stream's start event (isStreaming). Without this, agy's
|
|
739
|
-
// initial thinking seconds (before it emits any step) show nothing and the
|
|
740
|
-
// UI looks frozen. Lazy start (on first content) was the old behavior.
|
|
741
|
-
ensureStarted(stream, blocks);
|
|
742
|
-
|
|
743
|
-
const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
|
|
744
|
-
const key = sessionKey(options, cwd);
|
|
745
|
-
const existing = store.get(key);
|
|
746
|
-
const messageCount = context.messages.length;
|
|
747
|
-
|
|
748
|
-
const prompt = extractUserPrompt(context);
|
|
749
|
-
if (!prompt) {
|
|
750
|
-
finalize(stream, blocks, "error", "No user message to send to agy.");
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// Runtime config (mode, permissions, digest). Loaded fresh each turn so
|
|
755
|
-
// /agy toggles take effect immediately without a reload.
|
|
756
|
-
const config = loadConfig();
|
|
757
|
-
|
|
758
|
-
// G1: inject a delta digest of pi-side context agy was not spawned for
|
|
759
|
-
// (compaction summaries, other-provider turns), gated on config.digest:
|
|
760
|
-
// the digest changes every turn and defeats agy's prompt cache. agy keeps
|
|
761
|
-
// its own history; see docs/PI-BRIDGE-GAPS.md (G1).
|
|
762
|
-
const watermark = existing?.lastMessageCount ?? 0;
|
|
763
|
-
const digest = config.digest ? buildContextDigest(context.messages, watermark) : "";
|
|
764
|
-
const fullPrompt = digest ? `${DIGEST_PREAMBLE}\n\n${digest}\n\n---\n\n${prompt}` : prompt;
|
|
765
|
-
|
|
766
|
-
// Resolve the pi model id to its catalog entry. On a miss, fall through to
|
|
767
|
-
// the id itself - agy will likely reject, but the error reaches the user
|
|
768
|
-
// instead of a silent no-op.
|
|
769
|
-
const entry = entries.find((e) => e.id === model.id) ?? null;
|
|
770
|
-
const agyModel = entry?.full ?? model.id;
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
// Effort-driven bases always need --effort (a base slug is invalid on its
|
|
774
|
-
// own); fixed models never get it (agy rejects --effort for them). For an
|
|
775
|
-
// effort-driven base we clamp pi's level to the tiers agy offers it.
|
|
776
|
-
const effort = entry?.efforts?.length ? toAgyEffort(options?.reasoning, entry.efforts) : undefined;
|
|
777
|
-
|
|
778
|
-
const runOpts: AgyRunOptions = {
|
|
779
|
-
cwd,
|
|
780
|
-
model: agyModel,
|
|
781
|
-
mode: config.mode,
|
|
782
|
-
skipPermissions: config.skipPermissions,
|
|
783
|
-
effort,
|
|
784
|
-
prompt: fullPrompt,
|
|
785
|
-
conversationId: existing?.conversationId ?? null,
|
|
786
|
-
baseStepIdx: existing?.lastStepIdx ?? -1,
|
|
787
|
-
timeoutMin: DEFAULT_TIMEOUT_MIN,
|
|
788
|
-
signal: options?.signal,
|
|
789
|
-
};
|
|
790
|
-
|
|
791
|
-
// G8: per-turn diff context for agy's file edits (write_to_file et al.).
|
|
792
|
-
// Turn-scoped so concurrent turns never share OLD-content caches.
|
|
793
|
-
const diffCtx = new TurnDiffContext(createExecGitOps());
|
|
794
|
-
|
|
795
|
-
const onEvent = (event: AgyEvent) => {
|
|
796
|
-
switch (event.kind) {
|
|
797
|
-
case "text":
|
|
798
|
-
appendTextDelta(event.text);
|
|
799
|
-
break;
|
|
800
|
-
case "thinking":
|
|
801
|
-
appendThinkingDelta(event.text);
|
|
802
|
-
break;
|
|
803
|
-
case "tool": {
|
|
804
|
-
// G8: if agy wrote a file, surface a git-sourced diff; else the plain
|
|
805
|
-
// tool label. Always shown (agy's own tool loop, surfaced for visibility).
|
|
806
|
-
const edit = parseEditToolInput(event.inputJson ?? "");
|
|
807
|
-
if (edit) {
|
|
808
|
-
const absFile = path.isAbsolute(edit.file) ? edit.file : path.resolve(cwd, edit.file);
|
|
809
|
-
const outcome = diffCtx.diffEdit(absFile, edit.content);
|
|
810
|
-
const label = edit.description ?? path.basename(absFile);
|
|
811
|
-
appendThinkingDelta(`[agy edit: ${label}]\n`);
|
|
812
|
-
if (outcome.text) appendThinkingDelta(`${outcome.text}\n`);
|
|
813
|
-
} else {
|
|
814
|
-
appendThinkingDelta(`[agy tool: ${event.name}]\n`);
|
|
815
|
-
}
|
|
816
|
-
break;
|
|
817
|
-
}
|
|
818
|
-
case "title":
|
|
819
|
-
// Conversation title metadata - not streamed to the user.
|
|
820
|
-
break;
|
|
821
|
-
}
|
|
822
|
-
};
|
|
823
|
-
|
|
824
|
-
let result;
|
|
825
|
-
try {
|
|
826
|
-
result = await runFn(runOpts, onEvent);
|
|
827
|
-
} catch (err) {
|
|
828
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
829
|
-
finalize(stream, blocks, "error", `agy failed to start: ${msg}`);
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// Persist for the next turn (resume). Only bind when we actually discovered
|
|
834
|
-
// an id - a discovery miss shouldn't clobber a prior good binding.
|
|
835
|
-
// Persist for the next turn (resume). Only bind when we actually discovered
|
|
836
|
-
// an id - a discovery miss shouldn't clobber a prior good binding. The
|
|
837
|
-
// lastMessageCount watermark advances on a successful bind even if the turn
|
|
838
|
-
// later aborted or timed out: the prompt (digest included) was handed to
|
|
839
|
-
// agy at spawn, so its DB has seen that context. Guarding this on
|
|
840
|
-
// exitCode===0 would re-inject stale deltas after retryable failures.
|
|
841
|
-
if (result.conversationId) {
|
|
842
|
-
store.set(key, {
|
|
843
|
-
conversationId: result.conversationId,
|
|
844
|
-
lastStepIdx: result.lastIdx,
|
|
845
|
-
lastMessageCount: messageCount,
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
if (result.aborted) {
|
|
850
|
-
finalize(stream, blocks, "aborted", "Operation aborted");
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
if (result.timedOut) {
|
|
854
|
-
const note = `agy exceeded the ${runOpts.timeoutMin}m timeout`;
|
|
855
|
-
finalize(stream, blocks, "error", note);
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
if (result.exitCode !== 0) {
|
|
859
|
-
const detail = result.stderr.trim() || `agy exited with status ${result.exitCode}`;
|
|
860
|
-
finalize(stream, blocks, "error", detail);
|
|
861
|
-
return;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// Discovery miss: agy exited cleanly but we never bound a conversation id
|
|
865
|
-
// this turn (ambiguous snapshot, DB not created in time, or a prior session
|
|
866
|
-
// whose id failed CONV_ID_RE and silently fell through to fresh discovery).
|
|
867
|
-
// Guard on whether we bound THIS turn, not on whether a prior session
|
|
868
|
-
// existed - otherwise a corrupt existing entry re-opens the silent-empty-
|
|
869
|
-
// success hole the first review closed.
|
|
870
|
-
if (!result.conversationId) {
|
|
871
|
-
const detail =
|
|
872
|
-
"agy exited cleanly but its conversation database could not be bound. " +
|
|
873
|
-
"The run may have partially applied edits with no visible output.";
|
|
874
|
-
finalize(stream, blocks, "error", detail);
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
// Success. If no text ever streamed (agy did only tool work, or returned
|
|
879
|
-
// empty), emit an empty text block so pi has a well-formed assistant turn.
|
|
880
|
-
if (blocks.textIdx === null && blocks.thinkingIdx === null) {
|
|
881
|
-
ensureTextOpen(stream, blocks);
|
|
882
|
-
}
|
|
883
|
-
finalize(stream, blocks, "stop");
|
|
884
|
-
}
|
|
885
|
-
|
|
886
714
|
/** Signal the start of the assistant turn exactly once. `start` is
|
|
887
715
|
* turn-level (analogous to Anthropic's message_start), not per-block - the
|
|
888
716
|
* per-block signals are text_start / thinking_start. */
|