@estebanforge/pi-antigravity-bridge 1.8.6 → 1.8.7
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 +6 -0
- package/README.md +1 -1
- package/extensions/index.ts +44 -3
- package/package.json +1 -1
- package/src/poll-fold.ts +55 -0
- package/src/provider.ts +11 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.8.7] - 2026-10-21
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **Consecutive `bridge_poll_result` polls collapse into one counted transcript line.** Each poll used to append its own display-only entry (observed live: 60 `Poll git_commit` entries from a single wait, flushed in bursts when the parked round-trip resolved). Poll events now fold into a single ♊︎-marked `agy-poll-run` entry per consecutive run (`♊︎ bridge_poll_result Poll git_commit (x30)`; expanded view shows the poll span) with a live dim status counter while a run is open. Runs close on a target switch, the next non-poll display event, turn end, and agent end. Bridge footer statuses (`agy-native`, `agy-usage`, the poll counter) now render muted, matching pi-hostname's dim treatment.
|
|
10
|
+
|
|
5
11
|
## [1.8.6] - 2026-10-20
|
|
6
12
|
|
|
7
13
|
### Changed
|
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. 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.
|
|
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. Consecutive polls render as ONE ♊︎-marked counted line (`♊︎ bridge_poll_result Poll git_commit (x30)`) instead of one entry per poll, with a live muted status counter while a run is open.
|
|
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
|
|
package/extensions/index.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
formatPollAnswer,
|
|
49
49
|
type NativeDisplayEvent,
|
|
50
50
|
} from "../src/provider.js";
|
|
51
|
+
import { PollRunFolder, type PollRunData } from "../src/poll-fold.js";
|
|
51
52
|
import {
|
|
52
53
|
createShadowTool,
|
|
53
54
|
stripMarkerFields,
|
|
@@ -396,15 +397,52 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
396
397
|
}
|
|
397
398
|
return new Text(lines.join("\n"), 0, 0);
|
|
398
399
|
});
|
|
400
|
+
// Collapsed poll runs: one ♊︎-marked counted line per consecutive poll
|
|
401
|
+
// burst instead of one entry per poll.
|
|
402
|
+
pi.registerEntryRenderer<PollRunData>("agy-poll-run", (entry, { expanded }, theme) => {
|
|
403
|
+
const run = entry.data;
|
|
404
|
+
const count = run?.count ?? 0;
|
|
405
|
+
const label = `Poll ${run?.target ?? "bridge call"} (x${count}${run?.state === "failed" ? ", failed" : ""})`;
|
|
406
|
+
const lines = [`♊︎ ${theme.fg("toolTitle", POLL_TOOL_NAME)} ${theme.fg("muted", label)}`];
|
|
407
|
+
if (expanded && run) {
|
|
408
|
+
const time = (iso: string): string => new Date(iso).toLocaleTimeString();
|
|
409
|
+
lines.push(theme.fg("dim", `${count} ${count === 1 ? "poll" : "polls"}, ${time(run.firstAt)} → ${time(run.lastAt)}`));
|
|
410
|
+
}
|
|
411
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
412
|
+
});
|
|
399
413
|
}
|
|
400
414
|
let pendingNativeTools = 0;
|
|
415
|
+
const pollRuns = new PollRunFolder();
|
|
416
|
+
const closePollRun = (): void => {
|
|
417
|
+
const settled = pollRuns.flush();
|
|
418
|
+
if (settled) pi.appendEntry<PollRunData>("agy-poll-run", settled);
|
|
419
|
+
setDimStatus("agy-poll", undefined);
|
|
420
|
+
};
|
|
421
|
+
// Footer statuses render muted, matching pi-hostname's dim treatment.
|
|
422
|
+
const setDimStatus = (key: string, text: string | undefined): void => {
|
|
423
|
+
const ui = activeUi;
|
|
424
|
+
if (!ui) return;
|
|
425
|
+
ui.setStatus(key, text === undefined ? undefined : ui.theme.fg("dim", text));
|
|
426
|
+
};
|
|
401
427
|
const onNativeEvent = (event: NativeDisplayEvent): void => {
|
|
402
|
-
if (engine !== "acp"
|
|
428
|
+
if (engine !== "acp") return;
|
|
429
|
+
// Polls are bridge-local (never get a genuine Pi card); the mcpServer
|
|
430
|
+
// tag is unreliable on these frames, so the tool name discriminates.
|
|
431
|
+
if (event.name === POLL_TOOL_NAME) {
|
|
432
|
+
const view = event.callId ? roundTrips.poll(event.callId) : undefined;
|
|
433
|
+
const switched = pollRuns.add(view?.name ?? "unknown call", view?.state ?? "running", new Date());
|
|
434
|
+
if (switched) pi.appendEntry<PollRunData>("agy-poll-run", switched);
|
|
435
|
+
const open = pollRuns.open;
|
|
436
|
+
setDimStatus("agy-poll", open ? `♊︎ bridge_poll_result → ${open.target} (x${open.count})` : undefined);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
closePollRun();
|
|
440
|
+
if (event.mcpServer === "pi-bridge") return;
|
|
403
441
|
// MCP calls through our own pi-bridge already have genuine Pi tool cards;
|
|
404
442
|
// only native Antigravity / other MCP events need display-only entries.
|
|
405
443
|
if (event.status === "started") {
|
|
406
444
|
pendingNativeTools++;
|
|
407
|
-
|
|
445
|
+
setDimStatus("agy-native", `agy ${event.name}… (${pendingNativeTools})`);
|
|
408
446
|
return;
|
|
409
447
|
}
|
|
410
448
|
pendingNativeTools = Math.max(0, pendingNativeTools - 1);
|
|
@@ -439,6 +477,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
439
477
|
outcome.status === "OK" ? "info" : "warn",
|
|
440
478
|
);
|
|
441
479
|
roundTrips.failAll(FAIL_REASON_TURN_END);
|
|
480
|
+
// Closes a poll run that no display event flushed.
|
|
481
|
+
closePollRun();
|
|
442
482
|
// Stamp live escalations with the ended turn's conversation: the park
|
|
443
483
|
// pass returns before the provider's store.set, so a first-turn park
|
|
444
484
|
// has no other record of where its result must be re-delivered.
|
|
@@ -454,7 +494,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
454
494
|
pi.on("message_update", (event, ctx) => {
|
|
455
495
|
if (!ctx.hasUI || event.message.role !== "assistant" || event.message.provider !== "antigravity" || loadConfig().acp.usageEstimate === "off") return;
|
|
456
496
|
const { input, output } = event.message.usage;
|
|
457
|
-
const status = `ACP ≈ ${input} in / ${output} out
|
|
497
|
+
const status = ctx.ui.theme.fg("dim", `ACP ≈ ${input} in / ${output} out`);
|
|
458
498
|
if (status !== lastUsageStatus) {
|
|
459
499
|
lastUsageStatus = status;
|
|
460
500
|
ctx.ui.setStatus("agy-usage", status);
|
|
@@ -467,6 +507,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
467
507
|
pi.on("agent_end", (event, ctx) => {
|
|
468
508
|
clearUsageStatus(event, ctx);
|
|
469
509
|
pendingNativeTools = 0;
|
|
510
|
+
closePollRun();
|
|
470
511
|
if (ctx.hasUI) ctx.ui.setStatus("agy-native", undefined);
|
|
471
512
|
});
|
|
472
513
|
pi.on("session_shutdown", (event, ctx) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.7",
|
|
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/poll-fold.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Fold state machine for bridge_poll_result display events: consecutive
|
|
2
|
+
// polls collapse into ONE counted transcript entry instead of one entry per
|
|
3
|
+
// poll (observed live 2026-10-21: 60 "Poll git_commit" entries from a single
|
|
4
|
+
// wait, all flushed in bursts when the parked round-trip resolved).
|
|
5
|
+
//
|
|
6
|
+
// Runs close on a target switch, on any non-poll display event, or at turn
|
|
7
|
+
// end; the caller persists each closed run as an "agy-poll-run" entry.
|
|
8
|
+
|
|
9
|
+
export interface PollRunData {
|
|
10
|
+
/** The pi tool the model kept polling, e.g. "git_commit". */
|
|
11
|
+
target: string;
|
|
12
|
+
count: number;
|
|
13
|
+
/** Strongest poll outcome seen: running < done < failed. Sticky so a
|
|
14
|
+
* still-running poll after a terminal one cannot downgrade the summary. */
|
|
15
|
+
state: "running" | "done" | "failed";
|
|
16
|
+
firstAt: string;
|
|
17
|
+
lastAt: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const STATE_RANK: Record<PollRunData["state"], number> = { running: 0, done: 1, failed: 2 };
|
|
21
|
+
|
|
22
|
+
export class PollRunFolder {
|
|
23
|
+
#run: PollRunData | undefined;
|
|
24
|
+
|
|
25
|
+
/** The open run, for a live status-line counter. */
|
|
26
|
+
get open(): PollRunData | undefined {
|
|
27
|
+
return this.#run;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Fold one poll event. Returns a run to persist when this poll switched
|
|
31
|
+
* targets away from the open run; the new poll always starts or extends
|
|
32
|
+
* a run, so callers append the return value BEFORE reading `open`. */
|
|
33
|
+
add(target: string, state: PollRunData["state"], at: Date): PollRunData | undefined {
|
|
34
|
+
let closed: PollRunData | undefined;
|
|
35
|
+
if (this.#run && this.#run.target !== target) closed = this.#close();
|
|
36
|
+
if (!this.#run) {
|
|
37
|
+
this.#run = { target, count: 0, state: "running", firstAt: at.toISOString(), lastAt: at.toISOString() };
|
|
38
|
+
}
|
|
39
|
+
this.#run.count += 1;
|
|
40
|
+
this.#run.lastAt = at.toISOString();
|
|
41
|
+
if (STATE_RANK[state] > STATE_RANK[this.#run.state]) this.#run.state = state;
|
|
42
|
+
return closed;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Close and return the open run, if any. Idempotent. */
|
|
46
|
+
flush(): PollRunData | undefined {
|
|
47
|
+
return this.#run ? this.#close() : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#close(): PollRunData {
|
|
51
|
+
const run = this.#run as PollRunData;
|
|
52
|
+
this.#run = undefined;
|
|
53
|
+
return run;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/provider.ts
CHANGED
|
@@ -332,6 +332,9 @@ export interface NativeDisplayEvent {
|
|
|
332
332
|
name: string;
|
|
333
333
|
mcpServer?: string;
|
|
334
334
|
status: "started" | "completed" | "failed";
|
|
335
|
+
/** Bridge callId from the tool args (poll calls carry one); lets the
|
|
336
|
+
* extension resolve the polled pi tool via the escalation registry. */
|
|
337
|
+
callId?: string;
|
|
335
338
|
path?: string;
|
|
336
339
|
command?: string;
|
|
337
340
|
output?: string;
|
|
@@ -1143,10 +1146,16 @@ export function consumeActivity(
|
|
|
1143
1146
|
// model-facing result is produced. Persist a card only on completion.
|
|
1144
1147
|
if (feats.engine === "acp" && feats.onNativeEvent) {
|
|
1145
1148
|
try {
|
|
1146
|
-
feats.onNativeEvent({ name: activity.name, mcpServer: activity.mcpServer, status: "started" });
|
|
1149
|
+
feats.onNativeEvent({ name: activity.name, mcpServer: activity.mcpServer, status: "started", callId: callIdArg(activity.args) });
|
|
1147
1150
|
} catch { /* UI failure must not fail the generation. */ }
|
|
1148
1151
|
}
|
|
1149
1152
|
return "continue";
|
|
1153
|
+
/** Bridge callId arg of an MCP tool call (bridge_poll_result requires it). */
|
|
1154
|
+
function callIdArg(args: Record<string, unknown>): string | undefined {
|
|
1155
|
+
const v = args.callId;
|
|
1156
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1150
1159
|
/** File-path argument of an ACP edit tool (observed: `file_path`); other
|
|
1151
1160
|
* tool kinds (execute, read) don't carry one. Returns undefined for
|
|
1152
1161
|
* non-edit tools. */
|
|
@@ -1183,7 +1192,7 @@ function acpEditFileArg(args: Record<string, unknown>): string | undefined {
|
|
|
1183
1192
|
let displayed = false;
|
|
1184
1193
|
try {
|
|
1185
1194
|
if (feats.onNativeEvent) {
|
|
1186
|
-
feats.onNativeEvent({ name: activity.name, mcpServer: activity.mcpServer, status: "completed", path: file, command, output: activity.output, diff: diffText });
|
|
1195
|
+
feats.onNativeEvent({ name: activity.name, mcpServer: activity.mcpServer, status: "completed", path: file, command, output: activity.output, diff: diffText, callId: callIdArg(activity.args) });
|
|
1187
1196
|
displayed = true;
|
|
1188
1197
|
}
|
|
1189
1198
|
} catch { /* A stale renderer must never fail the model turn. */ }
|