@estebanforge/pi-antigravity-bridge 1.8.4 → 1.8.6

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,22 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.8.6] - 2026-10-20
6
+
7
+ ### Changed
8
+
9
+ - **Declare native subagent masking explicitly in the staged system-prompt carrier (`subagent: false`, `agents: []`; issue #7).** Agent-file presence already suppresses the native subagent tools, but that was an accident of staging; the frontmatter now states it so provider sessions do not depend on an undocumented engine behavior (probe attribution of presence vs keys is untested). Sessions without the carrier (`systemPrompt: false` or a custom agent) keep native tools; the tool-priority note steers the model to bridge delegation where a delegation tool is listed.
10
+
11
+ ## [1.8.5] - 2026-10-20
12
+
13
+ ### Fixed
14
+
15
+ - **Skip `call_mcp_tool` replay and guard synthetic `antigravity` toolUse against inactive whitelist in subagents (issue #6).** In subagent sessions with strict tool whitelists, unconditional wrap replay caused 39 consecutive `Tool antigravity not found` errors. In parent sessions, Antigravity's internal `call_mcp_tool` steps generated duplicate dummy tool calls returning `(agy recorded no output)`. The bridge now drops `call_mcp_tool` from wrap replay and checks `feats.nativeActive("antigravity")` before emitting synthetic toolUse.
16
+
17
+ ### Changed
18
+
19
+ - **Subagent delegation routes through pi and holds a longer synchronous window (issue #6).** Antigravity orchestrators picked agy's native `invoke_subagent`/`run_subagent`/`manage_subagents`, whose spawns run inside agy, never surface in pi, and leave pi-side harnesses at "0 active / 0 finished". The tool-priority note now mandates bridged delegation tools when one exists (conditional, so a session without one keeps the native fallback), and delegation-shaped bridge calls (`subagent`/`subagent_run`-style tools, the Claude Code-style exact `Agent`, pi's `agent_spawn`/`agent_wait` family, `*delegate*`) hold the synchronous window 120s instead of 20s: the 30-120s subagent range now returns a plain tool result while staying under agy's ~180s `tools/call` abandonment, and longer runs escalate to the existing poll path. `escalateLongTools` in config.json (env `AGY_ESCALATE_LONG_TOOLS`, comma-separated) adds custom names; `escalateAfterMs: 0` still disables the early ack.
20
+
5
21
  ## [1.8.4] - 2026-10-15
6
22
 
7
23
  ### Security
package/README.md CHANGED
@@ -42,7 +42,7 @@ The bridge starts a localhost MCP server inside pi's process. The exposed catalo
42
42
 
43
43
  **Upstream pi APIs only.** 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.
44
44
 
45
- **Long calls don't die.** agy's MCP client abandons a `tools/call` request at a flat ~180s, which used to kill any pi tool that ran longer (a long peer review, a build, a commit preview waiting for you). A call still running after ~20 seconds now settles its HTTP request with a `STILL RUNNING` answer carrying a `callId` while pi keeps executing; agy fetches the result through the bridge-local `bridge_poll_result` tool and polls until it lands. Escalated calls get their own 30-minute budget, so human-gated tools can take as long as the human takes. Fast calls stay fully synchronous and never see any of this. If a park does fail (abort, timeout, recycle), the late result is re-routed to agy as a follow-up prompt in the same conversation instead of being lost.
45
+ **Long calls don't die.** agy's MCP client abandons a `tools/call` request at a flat ~180s, which used to kill any pi tool that ran longer (a long peer review, a build, a commit preview waiting for you). A call still running after ~20 seconds now settles its HTTP request with a `STILL RUNNING` answer carrying a `callId` while pi keeps executing; agy fetches the result through the bridge-local `bridge_poll_result` tool and polls until it lands. Delegation-shaped tools (pi subagent/delegation tools, e.g. `subagent_run`, `agent_spawn`) hold the synchronous window 120s instead (issue #6: orchestrators expect a plain result across the 30-120s subagent range, and 120s stays under the 180s wall); `escalateLongTools` in config.json adds custom names. Escalated calls get their own 30-minute budget, so human-gated tools can take as long as the human takes. Fast calls stay fully synchronous and never see any of this. If a park does fail (abort, timeout, recycle), the late result is re-routed to agy as a follow-up prompt in the same conversation instead of being lost.
46
46
 
47
47
  **Recursion safety.** Only the provider's agy receives the extra `--add-dir`. `AskAntigravity` is also filtered from the exposed tool list. Its delegated `agy -p` spawns with just the workspace, and for the whole delegated run the bridge hides its global per-pid entries: disabled before the spawn, released only on process close or error - agy watches the config and hot-reloads MCP servers on file changes, so a mid-run re-enable would poke the live delegation back into pi. A cross-process marker (`suppression.json` in the bridge's extensions-data dir) coordinates concurrent delegations: entries come back only when no live delegation remains, a session starting mid-delegation registers its own entry disabled, and the session-start heal re-enables only once the last delegator is gone (dead entries pruned by pid liveness with a 24h age bound). The inner agy therefore sees no bridge and cannot re-enter pi. Foreign servers in the global config are never touched.
48
48
 
@@ -119,7 +119,7 @@ If `agy models` fails at load (binary missing, auth not done, network stall), a
119
119
  | `webTools` | `off` (no web tools), `on` (register `agy_web_search` + `agy_read_url` as Pi tools, usable by ANY provider's model). Each call spawns a one-shot search-only `agy` agent (plan mode, bridge MCP inheritance off), gates the answer on an observed native `search_web`/`read_url_content` step, and spends Antigravity quota. Off by default: Antigravity sessions already have native web tools on both engines; these serve other providers | `off` |
120
120
  | `bridgeTools` | `none` (bridge off), `all` (every non-builtin tool, incl. other `Ask*` delegations), `mcp` (pi-mcp-adapter tools + skills bridge only) | `all` |
121
121
  | `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` |
122
- | `systemPrompt` | `on` (send pi's system prompt - operating instructions plus the global agent-dir `AGENTS.md` and ancestor `AGENTS.md`/`CLAUDE.md` - to the first prompt of each new agy conversation, plus a tool-priority note: Pi Bridge tools win over agy's native interactive ones, which never reach the user. Delivery: stream-json stages it as an agy agent file via `--agent`, off the prompt line - the CLI silently drops single prompts past a ~25KB cap and the dropped turn kills the conversation; ACP prepends it to the first prompt) or `off` (agy-native behavior) | `on` |
122
+ | `systemPrompt` | `on` (send pi's system prompt - operating instructions plus the global agent-dir `AGENTS.md` and ancestor `AGENTS.md`/`CLAUDE.md` - to the first prompt of each new agy conversation, plus a tool-priority note: Pi Bridge tools win over agy's native interactive ones, which never reach the user. Delivery: stream-json stages it as an agy agent file via `--agent`, off the prompt line - the CLI silently drops single prompts past a ~25KB cap and the dropped turn kills the conversation; the carrier declares `subagent: false` + `agents: []` so the native subagent tools stay masked (ACP carries no agent file, so its natives remain and the tool-priority note steers the model to pi delegation). ACP prepends it to the first prompt) or `off` (agy-native behavior) | `on` |
123
123
  | `turnTimeoutMin` | Overall cap on ONE agy turn, minutes, both engines. When it fires, the bridge kills the agy process mid-task ("ACP turn exceeded the 10m deadline" / "agy exceeded the 10m turn timeout") - it is a bridge cap, not a Google server limit. Default is TTY-aware: `0` (no cap) on an interactive pi - you are the backstop with Esc; `20` headless, where nobody can abort and one runaway turn blocks the serialized turn queue. Opt into a gate with 1-1440 (free type via `config.json` or `/agy timeout <1-1440|off>`, or the `/agy` picker's preset list); `0` disables explicitly; anything else falls back to the TTY-aware default. The 5m inactivity stall guard always still bounds a hung server | `0` TTY / `20` headless |
124
124
  | `inactivityTimeoutMin` | Silence cap, minutes, both engines: no stream-json stdout / no ACP session/update for this long fails the turn as a stall. `0` disables the guard | `5` |
125
125
 
@@ -43,6 +43,7 @@ import {
43
43
  ToolRoundTrips,
44
44
  WrapperReplay,
45
45
  createStreamSimple,
46
+ defaultEscalateAfterMs,
46
47
  formatEscalatedAck,
47
48
  formatPollAnswer,
48
49
  type NativeDisplayEvent,
@@ -362,6 +363,9 @@ export default async function (pi: ExtensionAPI): Promise<void> {
362
363
  d,
363
364
  level ?? (s === "round-trip-fail" ? "warn" : "debug"),
364
365
  ),
366
+ // Delegation tools hold the sync window 120s (issue #6); config adds
367
+ // extra long-running names, re-read per call so edits apply live.
368
+ { escalateAfterMs: (name) => defaultEscalateAfterMs(name, loadConfig().escalateLongTools) },
365
369
  );
366
370
  const replay = new WrapperReplay();
367
371
  // Subagent roster: folded from every driver activity (both engines) via
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-antigravity-bridge",
3
- "version": "1.8.4",
3
+ "version": "1.8.6",
4
4
  "description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. Use antigravity/* models in Pi. Full /model picker support. Models runs Pi's tools. ToS safe to use.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/config.ts CHANGED
@@ -142,6 +142,13 @@ export interface AgyConfig {
142
142
  * "mcp" surface serves an empty catalog on installs without
143
143
  * pi-mcp-adapter), "mcp" (pi-mcp-adapter tools + skills bridge only). */
144
144
  bridgeTools: BridgeTools;
145
+ /** Extra pi tool names that get the long (120s) synchronous escalation
146
+ * window, in addition to delegation-shaped names (subagent_run,
147
+ * agent_spawn/agent_wait, *delegate*; issue #6). For custom tools that
148
+ * legitimately run minutes-long but must return a plain result to an
149
+ * orchestrator. Env AGY_ESCALATE_LONG_TOOLS (comma-separated) wins over
150
+ * the file. Takes effect on the next bridge call. */
151
+ escalateLongTools?: string[];
145
152
  /** Inject a delta digest of pi-side context (compaction summaries, turns
146
153
  * handled by other providers or pi's own tools) into each agy prompt.
147
154
  *
@@ -318,6 +325,20 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
318
325
  ? bridgeRaw
319
326
  : DEFAULTS.bridgeTools;
320
327
 
328
+ // Extra long-window tool names for the bridge escalation (issue #6).
329
+ // Comma-separated in the env; junk entries and junk shapes are dropped,
330
+ // not fatal - loadConfig runs per bridge call and must never throw.
331
+ const longToolsRaw: unknown =
332
+ process.env.AGY_ESCALATE_LONG_TOOLS ?? file.escalateLongTools;
333
+ const longToolsParts: unknown[] = typeof longToolsRaw === "string"
334
+ ? longToolsRaw.split(",")
335
+ : Array.isArray(longToolsRaw) ? longToolsRaw : [];
336
+ const escalateLongTools = longToolsParts
337
+ .filter((s): s is string => typeof s === "string")
338
+ .map((s) => s.trim())
339
+ .filter((s) => s.length > 0)
340
+ .slice(0, 32);
341
+
321
342
  const digest = process.env.AGY_DIGEST !== undefined
322
343
  ? ["1", "true", "on"].includes(process.env.AGY_DIGEST.toLowerCase())
323
344
  : file.digest ?? false;
@@ -381,6 +402,7 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
381
402
  askTool,
382
403
  webTools,
383
404
  bridgeTools,
405
+ escalateLongTools,
384
406
  digest,
385
407
  systemPrompt,
386
408
  turnTimeoutMin,
package/src/provider.ts CHANGED
@@ -140,7 +140,7 @@ export const SYSTEM_PROMPT_END = "[END SYSTEM PROMPT]";
140
140
  * the user's machine must go through the bridge. Rides the systemPrompt
141
141
  * gate: the note ships only when the system prompt ships. */
142
142
  export const TOOL_PRIORITY_NOTE =
143
- "[Tool priority: this conversation runs inside pi, not as a standalone agy session; the user only sees what surfaces in pi. Native interactive tools, for example ask_question, never reach the user. When a Pi Bridge tool covers the same purpose, always use the Pi Bridge tool; for user questions use ask_user_question. Your native file tools such as view_file only read brain artifacts and reject real filesystem paths; for any path on the user's machine use the Pi Bridge tools (read, ls, grep, find, edit, execute). Long-running bridge calls do not fail: after ~20 seconds the bridge answers STILL RUNNING with a callId; fetch the result with bridge_poll_result and poll until it lands. For work you already know is long, prefer exec_command's session-output pattern or background agents so you keep working while it runs.]";
143
+ "[Tool priority: this conversation runs inside pi, not as a standalone agy session; the user only sees what surfaces in pi. Native interactive tools, for example ask_question, never reach the user. When a Pi Bridge tool covers the same purpose, always use the Pi Bridge tool; for user questions use ask_user_question. Your native file tools such as view_file only read brain artifacts and reject real filesystem paths; for any path on the user's machine use the Pi Bridge tools (read, ls, grep, find, edit, execute). Delegate subagent work the same way: when pi's tool list includes a delegation tool (for example subagent, Agent, agent_spawn, agent_wait, or subagent_run), always delegate through it and never through the native invoke_subagent, run_subagent, or manage_subagents; use native ones only if no pi delegation tool is listed, because native subagents run inside agy and are invisible to pi and anything watching it. Long-running bridge calls do not fail: after ~20 seconds the bridge answers STILL RUNNING with a callId; fetch the result with bridge_poll_result and poll until it lands. For work you already know is long, prefer exec_command's session-output pattern or background agents so you keep working while it runs.]";
144
144
 
145
145
  /** Assemble the full agy prompt: system prompt block, pi-side digest, user
146
146
  * prompt. Empty parts are dropped. Pure; exported for unit testing.
@@ -418,6 +418,27 @@ export const POLL_TOOL_NAME = "bridge_poll_result";
418
418
  /** Default quiet period before a park escalates to a poll handle. Well under
419
419
  * agy's ~180s request deadline; fast tools never see it. */
420
420
  export const ESCALATE_AFTER_MS = 20_000;
421
+
422
+ /** Synchronous window for delegation-shaped tools. pi subagents commonly run
423
+ * 30-120s (issue #6) and orchestrators expect a plain tool result; past this
424
+ * they escalate to the poll path. The ceiling stays under agy's ~180s
425
+ * tools/call abandonment, which would kill the request with no result and no
426
+ * poll handle. */
427
+ export const DELEGATION_ESCALATE_MS = 120_000;
428
+
429
+ /** Names that get the long synchronous window: pi's agent_spawn/agent_wait
430
+ * family, subagent_run-style tools, *delegate*, and the Claude Code-style
431
+ * exact name "Agent" (start-anchored stem so incidental agent_ substrings
432
+ * elsewhere in a name keep the fast ack). Human-gated tools
433
+ * (ask_user_question) deliberately stay on the fast ack: their poll path is
434
+ * load-bearing for permission flows. */
435
+ const DELEGATION_NAME_RE = /subagent|delegate|^agent(_|$)/i;
436
+
437
+ /** Per-call escalation threshold. Config can add extra long-running names
438
+ * (escalateLongTools). */
439
+ export function defaultEscalateAfterMs(name: string, extraLongNames: readonly string[] = []): number {
440
+ return DELEGATION_NAME_RE.test(name) || extraLongNames.includes(name) ? DELEGATION_ESCALATE_MS : ESCALATE_AFTER_MS;
441
+ }
421
442
  /** Escalated parks carry a longer TTL: human-gated tools (commit previews,
422
443
  * permission dialogs) legitimately block for many minutes. */
423
444
  export const ESCALATED_TIMEOUT_MS = 1_800_000;
@@ -647,7 +668,7 @@ export class ToolRoundTrips {
647
668
  * late-delivery path (see buildLateResultPrompt). */
648
669
  #dead = new Map<string, { name: string; reason: string }>();
649
670
  #escalations = new EscalationRegistry();
650
- #escalateAfterMs: number;
671
+ #escalateAfterMs: number | ((name: string) => number);
651
672
  #getDriver: () => TurnDriver;
652
673
  #log: (s: string, d?: unknown, level?: "debug" | "info" | "warn" | "error") => void;
653
674
  /** Approval park controls (mcp-server handle). Assigned by the extension
@@ -660,11 +681,11 @@ export class ToolRoundTrips {
660
681
  constructor(
661
682
  driver: TurnDriver | (() => TurnDriver),
662
683
  log?: (s: string, d?: unknown, level?: "debug" | "info" | "warn" | "error") => void,
663
- opts: { escalateAfterMs?: number } = {},
684
+ opts: { escalateAfterMs?: number | ((name: string) => number) } = {},
664
685
  ) {
665
686
  this.#getDriver = typeof driver === "function" ? driver : () => driver;
666
687
  this.#log = log ?? (() => {});
667
- this.#escalateAfterMs = opts.escalateAfterMs ?? ESCALATE_AFTER_MS;
688
+ this.#escalateAfterMs = opts.escalateAfterMs ?? defaultEscalateAfterMs;
668
689
  }
669
690
 
670
691
  get pendingIds(): string[] {
@@ -862,7 +883,8 @@ export class ToolRoundTrips {
862
883
  const onAbort = () => this.#fail(callId, "agy disconnected before the tool result arrived");
863
884
  signal.addEventListener("abort", onAbort, { once: true });
864
885
  const entry: PendingRoundTrip = { kind: "bridge", name, resolve, reject, timer, onAbort, signal };
865
- if (this.#escalateAfterMs > 0) {
886
+ const escalateAfterMs = typeof this.#escalateAfterMs === "function" ? this.#escalateAfterMs(name) : this.#escalateAfterMs;
887
+ if (escalateAfterMs > 0) {
866
888
  entry.escalateTimer = setTimeout(() => {
867
889
  const e = this.#pending.get(callId);
868
890
  // Resolved (or failed) between arm and fire: nothing to escalate.
@@ -878,7 +900,7 @@ export class ToolRoundTrips {
878
900
  }
879
901
  this.#escalations.escalate(callId, e.name);
880
902
  resolve({ escalated: true, callId, name: e.name });
881
- }, this.#escalateAfterMs);
903
+ }, escalateAfterMs);
882
904
  }
883
905
  this.#pending.set(callId, entry);
884
906
  // Strip the internal marker fields from model-supplied args (peer
@@ -1051,6 +1073,7 @@ export interface ActivityFeatures {
1051
1073
  engine?: "stream-json" | "acp";
1052
1074
  onNativeEvent?: (event: NativeDisplayEvent) => void;
1053
1075
  roster?: SubagentRoster;
1076
+ log?: (event: string, data?: unknown, level?: "debug" | "info" | "warn" | "error") => void;
1054
1077
  }
1055
1078
 
1056
1079
  /** Process-wide counter: round-trip ids must never repeat across turns in
@@ -1197,6 +1220,7 @@ function acpEditFileArg(args: Record<string, unknown>): string | undefined {
1197
1220
  // resume on the toolResult continuation. Without a replay store
1198
1221
  // (feature off) keep the label-only behavior.
1199
1222
  if (!feats.replay || !feats.roundTrips) return "continue";
1223
+ if (activity.name === "call_mcp_tool") return "continue";
1200
1224
  const mapped = mapAgyToolToNative(activity.name, activity.args);
1201
1225
  if (mapped && (!feats.nativeActive || feats.nativeActive(mapped.tool))) {
1202
1226
  const id = nextRtId("nat");
@@ -1209,6 +1233,12 @@ function acpEditFileArg(args: Record<string, unknown>): string | undefined {
1209
1233
  } as Record<string, JsonValue>);
1210
1234
  return "parked";
1211
1235
  }
1236
+ if (feats.nativeActive && !feats.nativeActive("antigravity")) {
1237
+ // Subagent sessions whitelist tools and drop the display wrapper;
1238
+ // emitting here would fail every card with "not found".
1239
+ feats.log?.("wrap-skip", { tool: activity.name }, "debug");
1240
+ return "continue";
1241
+ }
1212
1242
  {
1213
1243
  const id = nextRtId("wrap");
1214
1244
  feats.replay.set(id, activity.output ?? "(agy recorded no output)");
@@ -1464,6 +1494,7 @@ async function runTurnDriver(
1464
1494
  engine: deps.engine,
1465
1495
  onNativeEvent: deps.onNativeEvent,
1466
1496
  roster: deps.roster,
1497
+ log: deps.log,
1467
1498
  };
1468
1499
 
1469
1500
  const drainTurn = async (h: TurnHandle): Promise<"parked" | undefined> => {
@@ -11,8 +11,13 @@
11
11
  //
12
12
  // Frontmatter keeps agy's DEFAULTS on purpose (no excludeDefaultComponents,
13
13
  // no tools list, no inheritMcp override): provider sessions need the normal
14
- // toolset and the bridge MCP config for G9 round-trips. Contrast the ask-tool
15
- // reviewer agent, which strips everything for sandboxing.
14
+ // toolset and the bridge MCP config for G9 round-trips. The subagent keys are
15
+ // the one deliberate exception: agent-file presence already suppresses agy's
16
+ // native subagent tools (probed 2026-10-20, issue #7), and declaring it makes
17
+ // the suppression explicit instead of relying on the accident (whether the
18
+ // keys themselves mask, apart from presence, is unattributed).
19
+ // Contrast the ask-tool reviewer agent, which strips everything for
20
+ // sandboxing.
16
21
  //
17
22
  // Lifecycle: one agent dir per DISTINCT body (content-addressed by sha256);
18
23
  // two conversations with different system prompts in one process stage two
@@ -49,6 +54,8 @@ export function syspromptAgentMd(name: string, body: string): string {
49
54
  "description: \"Pi bridge system-prompt carrier: pi operating instructions plus project AGENTS.md context\"",
50
55
  "mainAgent: true",
51
56
  "model: inherit",
57
+ "subagent: false",
58
+ "agents: []",
52
59
  "---",
53
60
  "",
54
61
  body,