@estebanforge/pi-antigravity-bridge 1.2.5 → 1.3.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 CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.3.0] - 2026-08-31
6
+
7
+ ### Added
8
+
9
+ - Stream-json engine: one persistent `agy --input-format stream-json` process per provider; conversation binding from the `init` event (no more SQLite snapshot diffing), native tool-step events (no protobuf decoding), and token usage mapped onto pi's usage when agy reports it. `AGY_ENGINE=legacy-sqlite` keeps the old engine for one release.
10
+ - No-patch MCP tool bridge: bridge calls park in a round-trip store; the provider emits them as real pi `toolUse` turns, pi executes with native cards/permissions/hooks, and the toolResult completes the parked MCP response on the next stream call. `bridgeTools` config selects the surface: `none` / `mcp` (default) / `all`.
11
+ - Native re-execution of agy read-only tools as real pi builtins (native cards, live output).
12
+ - Display-only `antigravity` wrapper tool: mutating agy steps land as real toolCall/toolResult pairs via recorded-output replay.
13
+ - Skills bridge: `activate_skill` tool exposing the pi Agent Skills catalog to agy, answered by the bridge directly.
14
+ - `/agy doctor`: engine, bridge, driver counters, and lifecycle tail, zero tokens.
15
+ - Legacy patch cleanup: `src/patch-cleanup.ts` detects a leftover invokeTool patch, one-time notice on session start, and `/agy patch-cleanup` restores the original files from the versioned backup.
16
+
17
+ ### Changed
18
+
19
+ - The MCP tool bridge no longer patches pi. The `pi.invokeTool` round-trip is replaced by the provider-owned park/emit/resolve flow above.
20
+
21
+ ### Fixed
22
+
23
+ - Live stream-json protocol shapes against real agy: terminal status is `SUCCESS` (not `OK`) and agent text arrives as `text_delta`. The first burn-in turn failed on both; both are pinned by a regression test.
24
+ - Native re-exec tool calls include the `reasoning` argument pi requires on read/edit-class builtins; without it pi rejected every native `read` card at validation.
25
+ - Peer-review round 2 (engine): parked bridge calls suspend the stdout idle timer (a >5-minute permission prompt no longer kills the turn); turn lifetimes are serialized (a second `run()` can no longer orphan an open turn); the cumulative-text guard points the right direction; a settled turn fails round-trips parked against it.
26
+ - Peer-review round 2 (cleanup): backup selection prefers an exact version match over newest-by-mtime (stacked multi-version backups made legitimate restores refuse); `WrapperReplay` entries are single-use (no unbounded growth, no enumerable stale outputs); `rt`-kind round-trips are removed on turn death; the one-time leftover-patch notice is headless-safe (`ctx.hasUI` gate with stderr fallback) and set after surfacing, not before.
27
+
28
+ ### Removed
29
+
30
+ - `pi.invokeTool` patch: `src/patcher.ts`, the load-time consent prompt, `/agy patch` subcommands, `docs/PI-INVOKETOOL-PATCH.md`, and the `invokeToolPatchDeclined` config flag.
31
+
32
+
33
+ ## [1.2.6] - 2026-08-28
34
+
35
+ ### Changed
36
+
37
+ - **Offline fallback Flash bumped to Gemini 3.7.** `FALLBACK_MODELS`
38
+ (served only when `agy models` fails or is unauthenticated at load) now
39
+ offers `gemini-3.7-flash` instead of `gemini-3.6-flash`. Live discovery
40
+ always overrides the fallback, so this only affects the picker when agy
41
+ is missing or broken.
42
+
5
43
  ## [1.2.5] - 2026-08-24
6
44
 
7
45
  ### Fixed
package/README.md CHANGED
@@ -26,9 +26,9 @@ Residual limits (with or without the bridge):
26
26
 
27
27
  While agy is the active model it normally cannot see pi's universe of extensions: agentmemory, codegraph, web search, slack/asana, the `Ask*` delegations, and any other installed pi tool. This extension optionally bridges that gap.
28
28
 
29
- When the capability is present the bridge starts a localhost MCP server inside pi's process. `tools/list` returns pi's registered tools (built-in file/shell tools and `AskAntigravity` are filtered out), and `tools/call` executes them via `pi.invokeTool()`. agy discovers the server through a per-invocation config: the bridge writes `.agents/mcp_config.json` into a bridge-controlled dir (`~/.pi/agent/antigravity-bridge/agy-mcp-<pid>/`) and the provider passes that dir as an extra `--add-dir` when it spawns agy. The user's global agy config (`~/.gemini/config/mcp_config.json`) is never touched, so standalone agy outside pi is unaffected.
29
+ The bridge starts a localhost MCP server inside pi's process. `tools/list` returns pi's registered tools (built-in file/shell tools and `AskAntigravity` are filtered out), and `tools/call` executes them via `pi.invokeTool()`. agy discovers the server through a per-invocation config: the bridge writes `.agents/mcp_config.json` into a bridge-controlled dir (`~/.pi/agent/antigravity-bridge/agy-mcp-<pid>/`) and the provider passes that dir as an extra `--add-dir` when it spawns agy. The user's global agy config (`~/.gemini/config/mcp_config.json`) is never touched, so standalone agy outside pi is unaffected.
30
30
 
31
- **Capability requirement.** `pi.invokeTool()` is not (yet) part of upstream pi. On first load with the patch missing, the extension **asks you once** whether to apply it (see [Install](#install)); once applied it activates after a full `pi` restart. Without it the bridge detects the missing capability at load time, skips the MCP server, and everything else works unchanged. See [docs/PI-INVOKETOOL-PATCH.md](docs/PI-INVOKETOOL-PATCH.md) for what the patch is, and `/agy patch status|apply|restore` to inspect, force, or undo it.
31
+ **No patch required.** Bridge calls park in the provider's round-trip store; the provider ends the pi assistant message with a `toolUse` stop reason for the real pi tool, pi executes it in its own loop (native cards, permissions, hooks), and the toolResult completes the parked MCP response on the next stream call. This is the same mechanism tianzuo/pi-antigravity uses; upstream pi APIs only.
32
32
 
33
33
  **Recursion safety.** Only the provider's agy receives the extra `--add-dir`. The `AskAntigravity` tool spawns its own agy with just the workspace, so that inner agy starts plain (no pi tools) and cannot re-enter. `AskAntigravity` is also filtered from the exposed tool list. Standalone agy is unaffected because nothing is written to its global config.
34
34
 
@@ -36,6 +36,22 @@ When the capability is present the bridge starts a localhost MCP server inside p
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)
40
+
41
+ Read-only agy steps (view_file, list_dir, grep_search, find_by_name) re-run as
42
+ real pi builtins (`read`, `ls`, `grep`, `find`) when those builtins are active,
43
+ so their cards render with pi's own renderers. Mutating and agy-specialty steps
44
+ render through a display-only `antigravity` wrapper tool: its `execute()`
45
+ replays the output agy already recorded, so the transcript gets proper
46
+ toolCall/toolResult pairs without any double execution. Neither path re-runs
47
+ anything with side effects.
48
+
49
+ When the bridge is on, agy also gets one `activate_skill` tool whose enum is
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.
54
+
39
55
  ## Install
40
56
 
41
57
  > ⚠️ **Heads-up: this extension patches your `pi` install.** When it loads and
@@ -92,6 +108,18 @@ Model ids are slugified from the `agy models` output (`Gemini 3.6 Flash (Medium)
92
108
 
93
109
  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.
94
110
 
111
+ ### Engine and bridge surface
112
+
113
+ `config.json` selects the turn engine and the bridge surface:
114
+
115
+ | Key | Values | Default |
116
+ | --- | --- | --- |
117
+ | `engine` | `stream-json` (persistent agy process, toolUse round-trips, live usage) or `legacy-sqlite` (spawn `agy -p`, poll its SQLite) | `stream-json` |
118
+ | `bridgeTools` | `none` (bridge off), `mcp` (pi-mcp-adapter tools), `all` (every non-builtin tool, incl. other `Ask*` delegations) | `mcp` |
119
+ | `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` |
120
+
121
+ 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.
122
+
95
123
  ### The /agy command
96
124
 
97
125
  `/agy` configures the provider at runtime. Settings persist to `~/.pi/agent/antigravity-bridge/config.json` and take effect on the next turn.
@@ -16,7 +16,6 @@ src/sessions.ts atomic JSON store: pi session -> agy conversation + last s
16
16
  src/config.ts persisted runtime config (mode, permissions, model/thinking defaults)
17
17
  src/ask-tool.ts the AskAntigravity one-shot delegation tool (model/thinking defaults)
18
18
  src/mcp-server.ts MCP tool bridge: exposes pi's tools to agy over Streamable HTTP
19
- src/patcher.ts Auto-applies the pi.invokeTool local patch to enable the MCP tool bridge
20
19
  src/diff-render.ts render agy's file edits as git diffs in pi's thinking stream
21
20
  ```
22
21
 
@@ -36,11 +36,23 @@ import {
36
36
  type AgyModelEntry,
37
37
  } from "../src/models.js";
38
38
  import { SessionStore } from "../src/sessions.js";
39
- import { createStreamSimple } from "../src/provider.js";
40
- import { CONFIG_PATH, loadConfig, saveConfig, type AgyMode, type ThinkingTier } from "../src/config.js";
39
+ import { ToolRoundTrips, WrapperReplay, createStreamSimple } from "../src/provider.js";
40
+ import { AgyDriver } from "../src/driver.js";
41
+ import { CONFIG_PATH, loadConfig, saveConfig, type AgyMode, type BridgeTools, type ThinkingTier } from "../src/config.js";
41
42
  import { registerAskAntigravityTool, toolModelsFromRaw } from "../src/ask-tool.js";
42
- import { hasInvokeTool, startMcpServer, type McpServerHandle } from "../src/mcp-server.js";
43
- import { applyInvokeToolPatch, decidePatchAction, patchStatus, restorePatch } from "../src/patcher.js";
43
+ import { startMcpServer, type McpServerHandle } from "../src/mcp-server.js";
44
+ import {
45
+ ACTIVATE_SKILL_TOOL_NAME,
46
+ activateSkillSchema,
47
+ catalogSummary,
48
+ findSkillByName,
49
+ readSkillBody,
50
+ scanSkills,
51
+ type SkillLite,
52
+ } from "../src/skills.js";
53
+ import { mapAgyToolToNative } from "../src/native-tools.js";
54
+ import { Type } from "typebox";
55
+ import { patchStatus, restorePatch } from "../src/patch-cleanup.js";
44
56
 
45
57
  function resolveAgyBinary(): string {
46
58
  return process.env.AGY_BIN || "agy";
@@ -71,7 +83,26 @@ export default async function (pi: ExtensionAPI): Promise<void> {
71
83
  const models = entries.map(toPiModel);
72
84
 
73
85
  const store = new SessionStore();
74
- const streamSimple = createStreamSimple({ entries, store });
86
+ // Persistent stream-json engine + the no-patch pi-tool round-trip store.
87
+ // The MCP bridge parks calls here; the provider emits them as real pi
88
+ // toolUse turns and completes them from the next call's toolResult.
89
+ const driver = new AgyDriver();
90
+ const roundTrips = new ToolRoundTrips(driver);
91
+ const replay = new WrapperReplay();
92
+ // Native re-exec only emits for builtins actually active in the session;
93
+ // anything else (or an unknown name) falls back to the wrapper card.
94
+ const nativeActive = (name: string): boolean => {
95
+ try {
96
+ const getAll = (pi as unknown as { getAllTools: () => Array<{ name: string }> }).getAllTools.bind(pi);
97
+ return getAll().some((t) => t.name === name);
98
+ } catch {
99
+ return false;
100
+ }
101
+ };
102
+ // A settled turn cannot answer its parked calls; the driver never sees
103
+ // ToolRoundTrips, so the provider bridges the two here.
104
+ driver.onTurnEnd = () => roundTrips.failAll("antigravity turn ended with an unresolved pi tool call");
105
+ const streamSimple = createStreamSimple({ entries, store, driver, roundTrips, replay, nativeActive });
75
106
 
76
107
  pi.registerProvider("antigravity", {
77
108
  name: "Antigravity (agy)",
@@ -92,7 +123,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
92
123
  streamSimple,
93
124
  });
94
125
 
95
- registerAgyCommand(pi, { entries, store, usingFallback });
126
+ registerAgyCommand(pi, { entries, store, usingFallback, driver, getMcpPort: () => mcpHandle?.port ?? null });
96
127
 
97
128
  // AskAntigravity tool: one-shot delegation to agy (ported from
98
129
  // pi-ask-antigravity). When both extensions are installed, the bridge wins
@@ -100,13 +131,48 @@ export default async function (pi: ExtensionAPI): Promise<void> {
100
131
  // detects this package via import.meta.resolve).
101
132
  await registerAskAntigravityTool(pi, toolModels);
102
133
 
103
- // MCP tool bridge: expose pi's tools to agy over localhost Streamable HTTP,
104
- // backed by pi.invokeTool (local patch). Capability-gated: if the running pi
105
- // lacks invokeTool, startMcpServer returns { ok: false } and the bridge runs
106
- // unchanged (other instances do not break). Started on session_start so it is
107
- // live before the provider's first turn; torn down on session_shutdown.
134
+ // Display-only wrapper tool: the provider emits mutating agy steps as
135
+ // toolCalls against it (never re-executed - execute() replays the output
136
+ // agy already recorded). Empty description on purpose: no model should
137
+ // call it, it exists so pi renders proper toolCall/toolResult cards.
138
+ pi.registerTool({
139
+ name: "antigravity",
140
+ label: "Antigravity",
141
+ description: "",
142
+ parameters: Type.Object({
143
+ tool: Type.String({ description: "agy tool name that produced this step." }),
144
+ key: Type.String({ description: "Internal replay key. Do not fabricate." }),
145
+ }),
146
+ execute: async (_toolCallId, params) => {
147
+ const key = (params as { key?: string }).key ?? "";
148
+ const output = replay.take(key) ?? `(no recorded output for ${key})`;
149
+ return { content: [{ type: "text", text: output }], details: { replay: true } };
150
+ },
151
+ });
152
+
153
+ // MCP tool bridge: expose pi's tools to agy over localhost Streamable HTTP.
154
+ // Calls park in the provider's round-trip store and complete through pi's
155
+ // normal toolUse loop (native cards, permissions, hooks) - no patch, no
156
+ // privileged API. Started on session_start, torn down on session_shutdown.
108
157
  let mcpHandle: McpServerHandle | null = null;
109
158
  pi.on("session_start", async (_event, ctx) => {
159
+ // Legacy cleanup: users who ran the old consent-gated patcher still
160
+ // carry pi.invokeTool in their installed pi. Inert, but tell them once
161
+ // and offer /agy patch-cleanup. Never auto-edits the install.
162
+ try {
163
+ if (!loadConfig().patchCleanupNotified && patchStatus().present) {
164
+ // Flag after surfacing, not before: headless sessions log to
165
+ // stderr (ctx.ui.notify is a no-op without a UI), so the notice
166
+ // is never silently dropped.
167
+ const msg =
168
+ "Your pi install still carries the old pi.invokeTool patch. It is unused and harmless; a pi update also removes it. To restore the original files from the backup now: /agy patch-cleanup";
169
+ if (ctx.hasUI) ctx.ui.notify(msg, "info");
170
+ else console.error(`[antigravity-bridge] ${msg}`);
171
+ saveConfig({ patchCleanupNotified: true });
172
+ }
173
+ } catch {
174
+ /* detection is best-effort */
175
+ }
110
176
  // Bridge lifecycle/error logger. Routes through ctx.ui.notify (an
111
177
  // ephemeral toast that fades) instead of stderr: pi's TUI captures stderr
112
178
  // and pins it above the input for the whole session, which left the
@@ -131,79 +197,65 @@ export default async function (pi: ExtensionAPI): Promise<void> {
131
197
  console.error(msg);
132
198
  }
133
199
  };
134
- // Decide the pi.invokeTool patch + MCP tool bridge path. The patch is only
135
- // needed for the bridge; the provider and AskAntigravity tool always work.
136
- // The bridge can only start when the patch is LIVE in this process; a
137
- // just-applied patch needs a full pi restart (not /reload) to go live.
138
- const live = hasInvokeTool(pi);
139
- const action = live
140
- ? ({ kind: "proceed" } as const)
141
- : decidePatchAction(live, patchStatus().present, !!loadConfig().invokeToolPatchDeclined, !!ctx.hasUI);
142
-
143
- try {
144
- switch (action.kind) {
145
- case "proceed": {
146
- if (loadConfig().invokeToolPatchDeclined) {
147
- saveConfig({ invokeToolPatchDeclined: false });
148
- }
149
- break;
150
- }
151
- case "notify-restart": {
152
- ctx.ui.notify(
153
- "The pi.invokeTool patch is present on disk but not loaded in this pi session. Fully RESTART pi (quit + relaunch) to start the MCP tool bridge.",
154
- "warning",
155
- );
156
- break;
157
- }
158
- case "silent": {
159
- // Previously declined. Stay quiet; bridge stays off until the user
160
- // runs /agy patch apply or the patch becomes live.
161
- mcpLog("patch-declined");
162
- break;
163
- }
164
- case "ask": {
165
- const apply = await ctx.ui.confirm(
166
- "Apply the pi.invokeTool patch?",
167
- "Enables the MCP tool bridge. Edits one method into your installed @earendil-works/pi-coding-agent/dist/ (reversible via /agy patch restore), and takes effect after a full pi restart.",
168
- // Bound the wait so a non-confirm-capable RPC client (hasUI is always
169
- // true in RPC) can't hang session_start. Timeout resolves like "no"
170
- // (declined persists); reversible via /agy patch apply.
171
- { timeout: 60_000 },
172
- );
173
- if (apply) {
174
- const res = applyInvokeToolPatch({ log: mcpLog });
175
- if (res.patched) {
176
- saveConfig({ invokeToolPatchDeclined: false });
177
- ctx.ui.notify(
178
- `Applied the pi.invokeTool patch to ${res.root} (pi ${res.version}). Fully RESTART pi (quit + relaunch) to start the MCP tool bridge.`,
179
- "warning",
180
- );
181
- } else if (res.errors.length > 0) {
182
- ctx.ui.notify(`pi.invokeTool patch failed: ${res.errors[0]}`, "error");
183
- }
184
- } else {
185
- saveConfig({ invokeToolPatchDeclined: true });
186
- ctx.ui.notify(
187
- "Skipped. The MCP tool bridge stays off. To enable it later, run /agy patch apply. Then restart pi.",
188
- "info",
189
- );
200
+ // Start the bridge unless the user turned it off. No patch gate, no
201
+ // consent flow: calls route through pi's normal toolUse loop.
202
+ const bridgeMode: BridgeTools = loadConfig().bridgeTools;
203
+ if (bridgeMode === "none") return; // user opted out
204
+ if (mcpHandle) return; // already running (reload re-fires session_start)
205
+ const SKIP = new Set(["AskAntigravity"]);
206
+ const skills: SkillLite[] = scanSkills(process.cwd());
207
+ const getAll = (pi as unknown as {
208
+ getAllTools: () => Array<{ name: string; description?: string; parameters?: object; sourceInfo?: { source?: string } }>;
209
+ }).getAllTools.bind(pi);
210
+ const listTools = () => {
211
+ const all = getAll();
212
+ const filtered =
213
+ bridgeMode === "mcp"
214
+ ? all.filter((t) => /pi-mcp-adapter/.test(t.sourceInfo?.source ?? ""))
215
+ : all.filter((t) => t.sourceInfo?.source !== "builtin");
216
+ const tools = filtered
217
+ .filter((t) => !SKIP.has(t.name))
218
+ .map((t) => {
219
+ let inputSchema: object = { type: "object", properties: {}, additionalProperties: true };
220
+ try {
221
+ if (t.parameters) inputSchema = JSON.parse(JSON.stringify(t.parameters)) as object;
222
+ } catch {
223
+ /* keep default schema */
190
224
  }
191
- break;
192
- }
193
- case "headless-skip": {
194
- console.error(
195
- "[antigravity-bridge] pi.invokeTool patch missing; MCP tool bridge off. Apply interactively (/agy patch apply) or re-run pi in a TUI.",
196
- );
197
- break;
198
- }
225
+ return { name: t.name, description: t.description ?? t.name, inputSchema };
226
+ });
227
+ if (skills.length > 0) {
228
+ tools.push({
229
+ name: ACTIVATE_SKILL_TOOL_NAME,
230
+ description: `Activate a pi Agent Skill by name. Catalog:\n${catalogSummary(skills)}`,
231
+ inputSchema: activateSkillSchema(skills) as object,
232
+ });
199
233
  }
200
- } catch (e) {
201
- mcpLog("self-patch-error", e instanceof Error ? e.message : String(e));
202
- }
203
-
204
- if (action.kind !== "proceed") return; // bridge can't start without a live patch
205
- if (mcpHandle) return; // already running (reload re-fires session_start)
206
- const r = await startMcpServer(pi, { log: mcpLog });
234
+ return tools;
235
+ };
236
+ // activate_skill never round-trips through pi: the bridge answers it
237
+ // directly by reading the SKILL.md (pi has no skill tool to execute).
238
+ const bridgeOnToolCall = (
239
+ callId: string,
240
+ name: string,
241
+ args: Record<string, unknown>,
242
+ signal: AbortSignal,
243
+ ) => {
244
+ if (name !== ACTIVATE_SKILL_TOOL_NAME) return roundTrips.onToolCall(callId, name, args, signal);
245
+ const wanted = typeof args.name === "string" ? args.name : "";
246
+ const skill = findSkillByName(skills, wanted);
247
+ const body = skill ? readSkillBody(skill) : `unknown skill: ${wanted || "(none given)"}`;
248
+ return Promise.resolve({
249
+ content: [
250
+ {
251
+ type: "text",
252
+ text: skill ? `${body}\n\n[skill resources dir: ${skill.dir}]` : `Error: ${body}`,
253
+ },
254
+ ],
255
+ isError: !skill,
256
+ });
257
+ };
258
+ const r = await startMcpServer({ listTools, onToolCall: bridgeOnToolCall }, { log: mcpLog });
207
259
  if (r.ok && r.handle) {
208
260
  mcpHandle = r.handle;
209
261
  } else {
@@ -214,6 +266,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
214
266
  const h = mcpHandle;
215
267
  mcpHandle = null;
216
268
  await h?.close();
269
+ roundTrips.failAll("antigravity session shut down");
270
+ await driver.close("shutdown");
217
271
  });
218
272
  }
219
273
 
@@ -223,6 +277,8 @@ interface AgyCommandCtx {
223
277
  entries: AgyModelEntry[];
224
278
  store: SessionStore;
225
279
  usingFallback: boolean;
280
+ driver: AgyDriver;
281
+ getMcpPort: () => number | null;
226
282
  }
227
283
 
228
284
  interface PendingConfig {
@@ -245,24 +301,19 @@ function statusText(ctx: AgyCommandCtx): string {
245
301
  ` tool thinking: ${config.defaultThinking}`,
246
302
  ` sessions: ${ctx.store.size} bound`,
247
303
  ` config: ${CONFIG_PATH}`,
248
- ` invokeTool: ${patchStateLabel()}`,
304
+ ` engine: ${config.engine}`,
305
+ ` bridge tools: ${config.bridgeTools}`,
306
+ ` digest: ${config.digest ? "on" : "off"}`,
249
307
  "",
250
- "Subcommands: /agy mode plan|accept-edits, /agy permissions on|off, /agy model flash|pro|gemini, /agy thinking low|medium|high, /agy patch status|apply|restore, /agy clear",
308
+ "Subcommands: /agy mode plan|accept-edits, /agy permissions on|off, /agy model flash|pro|gemini, /agy thinking low|medium|high, /agy clear",
251
309
  ].join("\n");
252
310
  }
253
311
 
254
- function patchStateLabel(): string {
255
- const s = patchStatus();
256
- if (s.present) return "patched";
257
- if (!s.root) return "MISSING (pi root not found)";
258
- if (loadConfig().invokeToolPatchDeclined) return `declined (pi ${s.version}). Resume: /agy patch apply`;
259
- return `MISSING (pi ${s.version}). Apply it: /agy patch apply`;
260
- }
261
312
 
262
313
  function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
263
314
  pi.registerCommand("agy", {
264
315
  description:
265
- "Antigravity provider: status, mode picker, patch, clear sessions. Usage: /agy [status|mode [plan|accept-edits]|patch [status|apply|restore]|clear]",
316
+ "Antigravity provider: status, doctor, mode picker, clear sessions. Usage: /agy [status|doctor|mode [plan|accept-edits]|digest on|off|patch-cleanup|clear]",
266
317
  handler: async (args, cmdCtx: ExtensionCommandContext) => {
267
318
  const ui = cmdCtx.ui;
268
319
  const mode = cmdCtx.mode;
@@ -275,6 +326,47 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
275
326
  ui?.notify("Cleared all antigravity session bindings.", "info");
276
327
  return;
277
328
  }
329
+ if (sub === "patch-cleanup") {
330
+ const st = patchStatus();
331
+ if (!st.present) {
332
+ ui?.notify(
333
+ st.root
334
+ ? `No invokeTool patch detected on pi ${st.version}. Nothing to clean.`
335
+ : "Could not locate the installed pi package. Nothing cleaned.",
336
+ "info",
337
+ );
338
+ return;
339
+ }
340
+ const r = restorePatch();
341
+ ui?.notify(
342
+ r.ok
343
+ ? `Restored ${r.restoredFiles.length} file(s) from ${r.backupDir}. The running session is unaffected; the files on disk are clean again.`
344
+ : `patch-cleanup failed: ${r.reason}`,
345
+ r.ok ? "info" : "error",
346
+ );
347
+ return;
348
+ }
349
+ if (sub === "doctor") {
350
+ const config = loadConfig();
351
+ const snap = ctx.driver.snapshot();
352
+ const port = ctx.getMcpPort();
353
+ const lines = [
354
+ "Antigravity doctor (no tokens spent)",
355
+ ` engine: ${config.engine}`,
356
+ ` bridge: ${config.bridgeTools}${port ? ` (port ${port})` : " (not running)"}`,
357
+ ` driver: ${snap.state}${snap.pid ? ` pid=${snap.pid}` : ""}${snap.conversationId ? ` conv=${snap.conversationId.slice(0, 8)}` : ""}`,
358
+ ` 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})` : ""}`,
359
+ ` sessions: ${ctx.store.size} bound`,
360
+ ` models: ${ctx.entries.length} ${ctx.usingFallback ? "FALLBACK (agy models failed)" : "discovered"}`,
361
+ ` config: ${CONFIG_PATH}`,
362
+ ];
363
+ if (snap.lifecycle.length > 0) {
364
+ lines.push(" lifecycle (last 5):");
365
+ for (const entry of snap.lifecycle.slice(-5)) lines.push(` ${entry}`);
366
+ }
367
+ ui?.notify(lines.join("\n"), "info");
368
+ return;
369
+ }
278
370
  if (sub === "mode") {
279
371
  if (val === "plan" || val === "accept-edits") {
280
372
  const next = saveConfig({ mode: val as AgyMode });
@@ -303,6 +395,21 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
303
395
  }
304
396
  return;
305
397
  }
398
+ if (sub === "digest") {
399
+ if (val === "on" || val === "off") {
400
+ const next = saveConfig({ digest: val === "on" });
401
+ ui?.notify(
402
+ next.digest
403
+ ? "digest on. pi-side context (compaction summaries, other-provider turns) is injected into each agy prompt. Note: this defeats agy's prompt cache (~25-30k tokens re-billed per turn)."
404
+ : "digest off. agy prompts contain only your message; agy's prompt cache stays stable. Enable when mixing providers in one session and agy must see pi-side context.",
405
+ "info",
406
+ );
407
+ } else {
408
+ ui?.notify(`digest: ${loadConfig().digest ? "on" : "off"}\nusage: /agy digest on|off`, "info");
409
+ }
410
+ return;
411
+ }
412
+
306
413
  if (sub === "thinking") {
307
414
  if (val === "low" || val === "medium" || val === "high") {
308
415
  const next = saveConfig({ defaultThinking: val as ThinkingTier });
@@ -313,53 +420,6 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
313
420
  return;
314
421
  }
315
422
 
316
- if (sub === "patch") {
317
- if (val === "restore") {
318
- const r = restorePatch();
319
- ui?.notify(
320
- r.ok
321
- ? `Restored ${r.restoredFiles.length} file(s) from ${r.backupDir}. Restart pi to take effect.`
322
- : `restore failed: ${r.reason}`,
323
- r.ok ? "info" : "error",
324
- );
325
- return;
326
- }
327
- if (val === "apply") {
328
- const r = applyInvokeToolPatch();
329
- if (r.patched || r.alreadyPresent) {
330
- saveConfig({ invokeToolPatchDeclined: false });
331
- }
332
- const msg = r.patched
333
- ? `Applied patch to ${r.changedFiles.length} file(s) in ${r.root} (pi ${r.version}). Restart pi to activate.`
334
- : r.alreadyPresent
335
- ? `Patch already present in ${r.root} (pi ${r.version}).`
336
- : `apply failed: ${r.errors[0] ?? "unknown error"}`;
337
- ui?.notify(msg, r.patched || r.alreadyPresent ? "info" : "error");
338
- return;
339
- }
340
- // status (default)
341
- const s = patchStatus();
342
- if (!s.root) {
343
- ui?.notify("patch status: could not locate the pi package root.", "warning");
344
- } else {
345
- ui?.notify(
346
- [
347
- `pi.invokeTool patch: ${s.present ? "PRESENT" : "MISSING"}`,
348
- ` root: ${s.root}`,
349
- ` version: ${s.version}`,
350
- s.missing.length ? ` missing: ${s.missing.length} site(s)` : null,
351
- loadConfig().invokeToolPatchDeclined ? " consent: declined. Resume: /agy patch apply" : null,
352
- s.backupDir ? ` backup: ${s.backupDir} (v${s.backupVersion})` : " backup: none",
353
- "",
354
- "Usage: /agy patch [status|apply|restore]",
355
- ]
356
- .filter(Boolean)
357
- .join("\n"),
358
- "info",
359
- );
360
- }
361
- return;
362
- }
363
423
 
364
424
  // No subcommand (or "status"): print status, or open the picker in TUI.
365
425
  if (sub && sub !== "status") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-antigravity-bridge",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
4
4
  "description": "Streaming Gemini provider for pi, built on the agy CLI. Registers antigravity/* models in pi's /model picker via SQLite polling + protobuf decode of agy's conversation DBs.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/config.ts CHANGED
@@ -24,6 +24,8 @@ 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
+ export type BridgeTools = "none" | "mcp" | "all";
27
29
 
28
30
  export interface AgyConfig {
29
31
  mode: AgyMode;
@@ -37,10 +39,33 @@ export interface AgyConfig {
37
39
  defaultModel: string;
38
40
  /** AskAntigravity tool: default thinking tier when the alias names none. */
39
41
  defaultThinking: ThinkingTier;
40
- /** User declined the pi.invokeTool auto-patch consent prompt. When true,
41
- * session_start silently skips the patch + MCP tool bridge until cleared
42
- * (by /agy patch apply succeeding, or the patch otherwise becoming live). */
43
- invokeToolPatchDeclined?: boolean;
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
+ /** Set after the one-time notice about a leftover legacy invokeTool patch
48
+ * on the installed pi. The notice never repeats; /agy patch-cleanup is
49
+ * always available. */
50
+ patchCleanupNotified?: boolean;
51
+ /** Which pi tools the MCP bridge exposes to agy: "none" (bridge off),
52
+ * "mcp" (pi-mcp-adapter tools + skills bridge; default), "all" (every
53
+ * registered non-builtin tool incl. other Ask* delegations). */
54
+ bridgeTools: BridgeTools;
55
+ /** Inject a delta digest of pi-side context (compaction summaries, turns
56
+ * handled by other providers or pi's own tools) into each agy prompt.
57
+ *
58
+ * Default OFF. The digest changes every turn, which defeats agy's
59
+ * server-side prompt cache: every turn re-bills the full context
60
+ * (~25-30k tokens observed). With it off, prompts stay stable and the
61
+ * cache hits.
62
+ *
63
+ * Enable when you mix providers in one pi session (Claude turns, pi-side
64
+ * tool runs, or a compaction that agy should know about) and you value
65
+ * agy seeing that context over the cache re-billing. Pure antigravity
66
+ * sessions gain nothing: agy already keeps its own history, and bridge
67
+ * round-trips deliver tool results through the bridge, not the digest. */
68
+ digest: boolean;
44
69
  }
45
70
 
46
71
  const DEFAULTS: AgyConfig = {
@@ -48,6 +73,9 @@ const DEFAULTS: AgyConfig = {
48
73
  skipPermissions: true,
49
74
  defaultModel: "flash",
50
75
  defaultThinking: "medium",
76
+ engine: "stream-json",
77
+ bridgeTools: "mcp",
78
+ digest: false,
51
79
  };
52
80
 
53
81
  /** Load config merged over defaults. Env vars override the file when set. */
@@ -92,7 +120,29 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
92
120
  const defaultThinking: ThinkingTier =
93
121
  thinkRaw === "low" || thinkRaw === "high" ? thinkRaw : "medium";
94
122
 
95
- return { mode, skipPermissions, defaultModel, defaultThinking, invokeToolPatchDeclined: file.invokeToolPatchDeclined };
123
+ const engine: AgyEngine =
124
+ process.env.AGY_ENGINE === "legacy-sqlite" || file.engine === "legacy-sqlite"
125
+ ? "legacy-sqlite"
126
+ : "stream-json";
127
+
128
+ const bridgeRaw = (process.env.AGY_BRIDGE_TOOLS ?? file.bridgeTools ?? DEFAULTS.bridgeTools).toLowerCase();
129
+ const bridgeTools: BridgeTools =
130
+ bridgeRaw === "none" || bridgeRaw === "all" ? bridgeRaw : "mcp";
131
+
132
+ const digest = process.env.AGY_DIGEST !== undefined
133
+ ? ["1", "true", "on"].includes(process.env.AGY_DIGEST.toLowerCase())
134
+ : file.digest ?? false;
135
+
136
+ return {
137
+ mode,
138
+ skipPermissions,
139
+ defaultModel,
140
+ defaultThinking,
141
+ engine,
142
+ bridgeTools,
143
+ digest,
144
+ patchCleanupNotified: file.patchCleanupNotified === true,
145
+ };
96
146
  }
97
147
 
98
148
  /** Atomically persist a config patch (temp + rename). */