@estebanforge/pi-antigravity-bridge 1.2.6 → 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.
@@ -0,0 +1,123 @@
1
+ // agy stream-json NDJSON event types + parser.
2
+ //
3
+ // `agy --input-format stream-json --output-format stream-json` emits one JSON
4
+ // object per line on stdout: `init` (conversation binding), `step_update`
5
+ // (user_input / agent_response / checkpoint / tool steps), `result` (terminal).
6
+ // Shapes captured from live output and cross-checked against
7
+ // tianzuo/pi-antigravity lib/events.ts (MIT). Unknown event kinds parse as
8
+ // {kind:"unknown"} so a future agy release degrades instead of crashing the
9
+ // reader loop.
10
+
11
+ export interface AgyUsage {
12
+ input_tokens?: number;
13
+ output_tokens?: number;
14
+ thinking_tokens?: number;
15
+ cache_read_tokens?: number;
16
+ total_tokens?: number;
17
+ }
18
+
19
+ export type AgyStepState = "ACTIVE" | "DONE" | "ERROR" | string;
20
+ export type AgyStepType = "user_input" | "checkpoint" | "agent_response" | "tool" | (string & {});
21
+
22
+ export interface AgyToolInfo {
23
+ name?: string;
24
+ parameters?: Record<string, unknown>;
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ export interface AgyStepUpdate {
29
+ step_index?: number;
30
+ state?: AgyStepState;
31
+ step_type?: AgyStepType;
32
+ conversation_id?: string;
33
+ tool_name?: string;
34
+ tool_info?: AgyToolInfo;
35
+ response_text?: string;
36
+ /** Live agy (1.1.13+): agent_response deltas arrive here, not in response_text. */
37
+ text_delta?: string;
38
+ thought_text?: string;
39
+ thinking_tokens?: number;
40
+ thinking_signature?: string;
41
+ duration_seconds?: number;
42
+ error_message?: string;
43
+ usage?: AgyUsage;
44
+ [key: string]: unknown;
45
+ }
46
+
47
+ export interface AgyResult {
48
+ status?: string;
49
+ response?: string;
50
+ error?: string;
51
+ usage?: AgyUsage;
52
+ conversation_id?: string;
53
+ [key: string]: unknown;
54
+ }
55
+
56
+ export type ParsedAgyEvent =
57
+ | { kind: "init"; conversationId?: string; usage?: AgyUsage }
58
+ | { kind: "step"; step: AgyStepUpdate }
59
+ | { kind: "result"; result: AgyResult }
60
+ | { kind: "unknown"; raw: unknown };
61
+
62
+ /** Parse one stdout line. Never throws; bad JSON / non-objects become "unknown". */
63
+ export function parseAgyLine(line: string): ParsedAgyEvent {
64
+ const trimmed = line.trim();
65
+ if (!trimmed) return { kind: "unknown", raw: null };
66
+ let obj: unknown;
67
+ try {
68
+ obj = JSON.parse(trimmed);
69
+ } catch {
70
+ return { kind: "unknown", raw: trimmed.slice(0, 200) };
71
+ }
72
+ if (typeof obj !== "object" || obj === null) return { kind: "unknown", raw: obj };
73
+ const rec = obj as Record<string, unknown>;
74
+ if (rec.event === "init" && typeof rec.init === "object" && rec.init !== null) {
75
+ const init = rec.init as Record<string, unknown>;
76
+ return {
77
+ kind: "init",
78
+ conversationId:
79
+ typeof init.conversation_id === "string" ? init.conversation_id : undefined,
80
+ usage: isUsage(init.usage) ? init.usage : undefined,
81
+ };
82
+ }
83
+ if (rec.event === "step_update" && typeof rec.step_update === "object" && rec.step_update !== null) {
84
+ return { kind: "step", step: rec.step_update as AgyStepUpdate };
85
+ }
86
+ if (rec.event === "result" && typeof rec.result === "object" && rec.result !== null) {
87
+ return { kind: "result", result: rec.result as AgyResult };
88
+ }
89
+ // Tolerate top-level shorthand: some agy builds put the payload at the root.
90
+ if (rec.event === "init") {
91
+ return {
92
+ kind: "init",
93
+ conversationId: typeof rec.conversation_id === "string" ? rec.conversation_id : undefined,
94
+ usage: isUsage(rec.usage) ? (rec.usage as AgyUsage) : undefined,
95
+ };
96
+ }
97
+ if (typeof rec.step_type === "string") return { kind: "step", step: rec as AgyStepUpdate };
98
+ if (typeof rec.status === "string") return { kind: "result", result: rec as AgyResult };
99
+ return { kind: "unknown", raw: obj };
100
+ }
101
+
102
+ function isUsage(u: unknown): u is AgyUsage {
103
+ return typeof u === "object" && u !== null;
104
+ }
105
+
106
+ /** Map agy usage onto pi-ai's Usage (cost stays zero: subscription quota). */
107
+ export function toPiUsage(
108
+ u: AgyUsage | undefined,
109
+ piUsage: {
110
+ input: number;
111
+ output: number;
112
+ cacheRead: number;
113
+ cacheWrite: number;
114
+ totalTokens: number;
115
+ cost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };
116
+ },
117
+ ): void {
118
+ if (!u) return;
119
+ piUsage.input = u.input_tokens ?? piUsage.input;
120
+ piUsage.output = u.output_tokens ?? piUsage.output;
121
+ piUsage.cacheRead = u.cache_read_tokens ?? piUsage.cacheRead;
122
+ piUsage.totalTokens = u.total_tokens ?? piUsage.input + piUsage.output;
123
+ }
@@ -1,254 +0,0 @@
1
- # The `pi.invokeTool` local patch
2
-
3
- > Status: **local, not upstream.** This is a patch to the installed `pi` package
4
- > that adds one method, `pi.invokeTool(name, args)`, to the extension API. It is
5
- > what lets the pi-antigravity-bridge expose pi's tools to agy over MCP. It is
6
- > small, self-contained, and gated by a runtime capability check, so pi runs
7
- > that lack it are unaffected. An upstream PR is the durable fix (planned).
8
-
9
- > **Self-applied by the extension.** As of v1.0.0, on first load with the patch
10
- > missing the bridge **asks you once** whether to apply it (`src/patcher.ts`); if
11
- > you decline it stays silent and won't ask again until `/agy patch apply`, so
12
- > you normally never touch these files by hand. The edits below are documented
13
- > for reference, auditing, and manual recovery. Auto-applier facts:
14
- > - Targets the **running** pi's `dist/` (located via `realpath(process.argv[1])`
15
- > and verified by anchor presence), never a sibling extension's decoy copy.
16
- > - Idempotent and two-phase: validates all anchors before writing, so a pi
17
- > version that moved the code aborts cleanly with nothing written. The facade
18
- > file is written **last**, so a crashed/partial patch leaves
19
- > `hasInvokeTool()` false (safe degraded), never a half-wired chain.
20
- > - Originals backed up under
21
- > `~/.pi/agent/antigravity-bridge/pi-patch-backup/<version>-<ts>-<pid>/`.
22
- > - `/agy patch status|apply|restore` inspects, forces, or rolls it back; restore
23
- > is version-guarded (refuses across pi versions, no silent downgrade).
24
- > - Takes effect only after a **full pi restart** (not `/reload`): pi caches its
25
- > compiled native-ESM core per process.
26
-
27
- This document is written for an LLM (or human) that needs to understand exactly
28
- what was changed, where, and why, without re-deriving it from the codebase.
29
-
30
- ## What it adds
31
-
32
- One new method on pi's `ExtensionAPI`:
33
-
34
- ```ts
35
- pi.invokeTool(name: string, args?: Record<string, unknown>, options?: {
36
- toolCallId?: string;
37
- signal?: AbortSignal;
38
- onUpdate?: (update: unknown) => void;
39
- }): Promise<{ content: unknown[]; details: unknown; isError?: boolean }>
40
- ```
41
-
42
- It looks up a registered tool by name and runs its `execute()` **out-of-band**
43
- (not inside an agent turn), returning the same `{ content, details, isError? }`
44
- shape the agent itself produces. Upstream pi already exposes tool *metadata*
45
- (`pi.getAllTools()`) but not tool *execution*; this is the missing sibling.
46
-
47
- ## Why the bridge needs it
48
-
49
- The bridge hosts an MCP server inside pi's process. agy connects to it and asks
50
- "what tools do you have?" (`tools/list`) and "run this tool" (`tools/call`).
51
- `tools/list` is served by the already-public `pi.getAllTools()`. `tools/call`
52
- must actually execute a pi tool, which requires the primitive this patch adds.
53
- Without it, the bridge can advertise pi's tools but cannot run them, so it
54
- detects the missing capability at load time and skips the MCP server entirely
55
- (see `hasInvokeTool()` in `src/mcp-server.ts`).
56
-
57
- ## The delegation chain (why there are 6 sites)
58
-
59
- pi's extension API is a facade. A call travels:
60
-
61
- ```
62
- pi.invokeTool() (facade, built in loader.js)
63
- -> runner.invokeTool() (ExtensionRunner, runner.js)
64
- -> runner.runtime.invokeTool() (shared runtime object)
65
- -> AgentSession.invokeTool() (the actions bundle wires this in bindCore)
66
- -> _toolRegistry.get(name).execute(...)
67
- ```
68
-
69
- `bindCore` copies session methods onto a shared `runtime` object that all
70
- extension APIs reference. That indirection is why a single method must be wired
71
- at **six** places: the implementation, the actions-bundle binding, the runner's
72
- copy, the runner's delegating method, the facade method, and the type.
73
-
74
- Since pi 0.84.3 there is a **seventh edit**, not part of the chain but the
75
- switch that makes the chain reachable: the entry redirect in
76
- `bundle/cli.js` (see Site 7).
77
-
78
- ## Where it goes (paths are relative to the pi package root)
79
-
80
- pi ships **compiled** (`dist/`); there is no `src/` to edit. Find the package
81
- root:
82
-
83
- ```bash
84
- node -e "console.log(require('path').dirname(require.resolve('@earendil-works/pi-coding-agent/package.json')))"
85
- # typical: ~/.npm-global/lib/node_modules/@earendil-works/pi-coding-agent
86
- ```
87
-
88
- All paths below are under `<package>/dist/`. Developed against pi `^0.82.1`;
89
- anchors verified against `0.84.3` (the `loader.js` facade switched to a local
90
- `assertActive()` guard in 0.84.3).
91
-
92
- ### Site 1 — `core/agent-session.js`: the implementation on `AgentSession`
93
-
94
- Added immediately after `getToolDefinition(name) { ... }`:
95
-
96
- ```js
97
- /**
98
- * LOCAL PATCH (pi-antigravity-bridge): invoke a registered tool by name
99
- * out-of-band and return its result. The tool wrapper synthesizes ctx via
100
- * its ctxFactory when none is passed. Not upstream pi (yet).
101
- */
102
- async invokeTool(name, args = {}, options = {}) {
103
- const tool = this._toolRegistry.get(name);
104
- if (!tool) {
105
- throw new Error(`invokeTool: tool "${name}" not found in registry`);
106
- }
107
- const toolCallId = options.toolCallId ?? `invokeTool:${name}:${Date.now()}`;
108
- return tool.execute(toolCallId, args, options.signal ?? undefined, options.onUpdate);
109
- }
110
- ```
111
-
112
- `_toolRegistry` is `Map<string, AgentTool>` and already exists on `AgentSession`.
113
- Stored tools are **wrapped** (`wrapRegisteredTools(..., runner)` in
114
- `tool-definition-wrapper.js`); their `execute` is
115
- `(toolCallId, params, signal, onUpdate, ctx) => definition.execute(..., ctx ?? ctxFactory?.())`.
116
- By passing only four args (no `ctx`), the wrapper synthesizes a valid
117
- `ExtensionContext` via the runner's `createContext()`. No per-turn agent state
118
- is required; this is what makes out-of-band execution safe.
119
-
120
- ### Site 2 — `core/agent-session.js`: the `bindCore` actions bundle (CRITICAL)
121
-
122
- Inside the object literal passed to `runner.bindCore({ ... })`, immediately
123
- after `refreshTools: () => this._refreshToolRegistry(),`:
124
-
125
- ```js
126
- invokeTool: (name, args, options) => this.invokeTool(name, args, options),
127
- ```
128
-
129
- **This is the easy site to miss.** `bindCore` is the only place session methods
130
- are wired onto the shared `runtime` object. Without this line,
131
- `actions.invokeTool` is `undefined`, so site 3 assigns `undefined`, and the
132
- facade throws `runtime.invokeTool is not a function`.
133
-
134
- ### Site 3 — `core/extensions/runner.js`: copy onto the shared runtime
135
-
136
- In `bindCore`, immediately after `this.runtime.refreshTools = actions.refreshTools;`:
137
-
138
- ```js
139
- this.runtime.invokeTool = actions.invokeTool;
140
- ```
141
-
142
- ### Site 4 — `core/extensions/runner.js`: the delegating method
143
-
144
- On `ExtensionRunner`, immediately after the `getActiveTools() { ... }` method:
145
-
146
- ```js
147
- invokeTool(name, args, options) {
148
- this.assertActive();
149
- return this.runtime.invokeTool(name, args, options);
150
- }
151
- ```
152
-
153
- ### Site 5 — `core/extensions/loader.js`: the facade method
154
-
155
- In the `api` object literal returned to extensions, immediately after the
156
- `getAllTools() { ... }` entry:
157
-
158
- ```js
159
- invokeTool(name, args, options) {
160
- assertActive();
161
- return runtime.invokeTool(name, args, options);
162
- },
163
- ```
164
-
165
- ### Site 6 — `core/extensions/types.d.ts`: the type declaration
166
-
167
- On the `ExtensionAPI` interface, immediately after `getAllTools(): ToolInfo[];`:
168
- ```ts
169
- /**
170
- * LOCAL PATCH (pi-antigravity-bridge): invoke a registered tool by name
171
- * out-of-band and return { content, details, isError? }. ctx is synthesized.
172
- */
173
- invokeTool(name: string, args?: Record<string, unknown>, options?: { toolCallId?: string; signal?: AbortSignal; onUpdate?: (update: unknown) => void }): Promise<{ content: unknown[]; details: unknown; isError?: boolean }>;
174
- ```
175
-
176
- ### Site 7 — `bundle/cli.js`: the entry redirect (pi 0.84.3+)
177
-
178
- pi 0.84.3 points its `bin` at `dist/bundle/cli.js`, a bundled runtime with its
179
- own embedded copy of the core. A process launched from the bundle never loads
180
- `dist/core/*`, so the six sites above stay inert. The fix is a full-file
181
- replacement of the tiny bundle entry with a shim that loads the still-shipped
182
- modular `dist/cli.js`:
183
-
184
- ```js
185
- #!/usr/bin/env node
186
- // LOCAL PATCH (pi-antigravity-bridge): redirect pi's bundled entry to the
187
- // modular runtime under dist/, where the invokeTool patch sites take effect.
188
- // Restore via /agy patch restore. See docs/PI-INVOKETOOL-PATCH.md.
189
- import "../cli.js";
190
- ```
191
-
192
- Tradeoff: pi starts through the modular runtime, giving up the bundle's faster
193
- startup. The patcher validates before writing that the file really is the
194
- bundled entry (it imports `chunks/`) and that `dist/cli.js` exists; on pre-bundle
195
- pi the file is absent and this site is skipped entirely.
196
-
197
- ## How to apply
198
-
199
- **You usually don't.** On first load, the extension asks you once whether to
200
- apply the patch (`src/patcher.ts`); see the note at the top of this document.
201
- The manual steps below are for reference, auditing, or recovering without the
202
- extension loaded.
203
-
204
- The patch is plain edits to the six compiled files above (plus the entry
205
- redirect on pi 0.84.3+). Because pi ships no `src/`, there is nothing to
206
- recompile. (The old "clear jiti's cache" step is a
207
- no-op on pi 0.82.1+ (including 0.84.3), which sets `moduleCache: false` in its
208
- extension loader, so jiti never writes an fs cache.)
209
-
210
- A `pi` reinstall or update overwrites `dist/` and **removes the patch**, but the
211
- extension re-applies it on the next start (then prompts you to restart pi).
212
- Durable fix: merge it upstream.
213
-
214
- ## How to verify
215
-
216
- 1. The facade has the method:
217
-
218
- ```bash
219
- pi -e /path/to/some-ext.ts --list-models # ext logs typeof pi.invokeTool
220
- # expect: "function"
221
- ```
222
-
223
- 2. It executes a real tool. Quickest: a tiny extension that on `session_start`
224
- calls `pi.invokeTool("read", { path: <some file> })` and prints the result,
225
- run via `pi -e that-ext.ts --mode rpc` (RPC mode fires `session_start`
226
- without a model turn). The returned `content` should hold the file text.
227
- (Note: print/`-p` mode hangs on a remote default model before `session_start`
228
- fires; RPC mode avoids that.)
229
-
230
- ## Capability gate (how the bridge stays safe without it)
231
-
232
- `src/mcp-server.ts` checks at load time:
233
-
234
- ```ts
235
- export function hasInvokeTool(pi: ExtensionAPI): boolean {
236
- return typeof (pi as unknown as { invokeTool?: unknown }).invokeTool === "function";
237
- }
238
- ```
239
-
240
- If false, `startMcpServer()` returns `{ ok: false }` immediately and the bridge
241
- runs exactly as it did before this feature existed: provider + `AskAntigravity`
242
- tool, no MCP server. So an unpatched pi, or a pi updated past the patch, degrades
243
- gracefully rather than crashing.
244
-
245
- ## Scope and risk
246
-
247
- - Adds one read-only-by-convention method. Does not modify existing behavior,
248
- tool registration, the agent loop, or tool results.
249
- - Calls the existing wrapped `execute` with a synthesized ctx; tools that depend
250
- on live per-turn agent state (an active abort signal, the current message)
251
- get a quiescent ctx. This is fine for the stateless tools the bridge exposes
252
- (memory, codegraph, search, delegations) and is the same synthesis pi itself
253
- uses for out-of-band tool execution.
254
- - No new dependencies; no changes to persisted state or session format.