@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 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. The default `stream-json` engine runs one persistent agy process in your workspace, feeds it each turn, parses its stream-json events, and streams the agent text back into pi token by token. Token usage is live. A `legacy-sqlite` fallback engine (spawn `agy -p`, poll its SQLite conversation DB, decode its protobuf steps) still exists; see [Engine and bridge surface](#engine-and-bridge-surface).
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 on the `stream-json` engine; the `legacy-sqlite` fallback reports usage as zero.
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 (stream-json engine)
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 engine state,
52
- driver counters, bridge port, and the last lifecycle events without spending
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
- ### Engine and bridge surface
86
+ ### Bridge surface
90
87
 
91
- `config.json` selects the turn engine and the bridge surface:
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: `AGY_ENGINE`, `AGY_BRIDGE_TOOLS`, `AGY_DIGEST`. The `legacy-sqlite` engine is kept as a fallback and is scheduled for removal once the stream-json engine has burned in.
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 engine state, driver counters, bridge port, last lifecycle events
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 engine table)
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 on the default engine, its SQLite conversation DB on the fallback engine. 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.
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
 
@@ -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
- ## Engines
5
+ ## Turn engine
6
6
 
7
- The provider runs one of two turn engines (`config.engine`, default `stream-json`):
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
- - **`stream-json` (default):** one 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.
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/runner.ts legacy engine: spawn agy -p, concurrent poll loop, abort/timeout, emit events
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 (engine, bridgeTools, digest, mode, permissions, model/thinking defaults)
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 native SQLite dependency (`node:sqlite` covers the legacy engine's reads).
30
+ No generated protobuf code, no SQLite dependency.
37
31
 
38
- ## Stream-json engine (default)
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
- ## Legacy-sqlite engine internals
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
- When **more than one** new file appears (a concurrent agy or subagent started in parallel), the runner passes its spawned pid to `newConversationId`, which scans the process tree's open file descriptors (`/proc/<pid>/fd` on Linux) to find the single candidate `.db` our own agy is writing to. This is the authoritative disambiguator: mtime cannot separate two *active* concurrent runs, and the user-message payload (`step_type 98`) is undocumented and deeply nested, so content-matching would risk a silent misbind. When the pid is unavailable, the platform is not Linux, the process has already exited (the AskAntigravity tool binds post-exit), or the scan itself is ambiguous, discovery fails safe to null rather than guessing.
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.
@@ -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 (decoder? poller? provider? pi loader?).
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 (both engines). 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.
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. On `legacy-sqlite` the snapshot/diff discovery refuses to bind if more than one new `.db` appears (ambiguous). `npm run decode-db` against the suspected DB confirms agy wrote steps.
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 runner (no agy) and asserts the exact event sequence: text/thinking close-on-switch, tool labels through the thinking block, empty-turn fallback.
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, legacy decode/polling).
58
+ See [ARCHITECTURE.md](./ARCHITECTURE.md) for the module map and the engine internals (stream-json events, no-patch round-trip).
@@ -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 spawns `agy -p`, polls the conversation SQLite DB
6
- // agy writes, decodes the protobuf step payloads, and streams the agent text
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.1",
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
- /** Override the agy turn runner (tests inject a scripted event source).
249
- * Defaults to the real runAgyTurn. Legacy engine only. */
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, runAgyTurn: runFn = runAgyTurn, driver, roundTrips } = deps;
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
- const config = loadConfig();
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
- void runTurn(stream, model, context, options, entries, store, runFn);
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. */