@estebanforge/pi-antigravity-bridge 1.4.5 → 1.4.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 +32 -1
- package/README.md +47 -4
- package/docs/ARCHITECTURE.md +1 -0
- package/docs/DEVELOPMENT.md +1 -0
- package/extensions/index.ts +115 -12
- package/package.json +1 -1
- package/src/acp/auth.ts +12 -2
- package/src/acp/driver.ts +8 -0
- package/src/ask-tool.ts +15 -0
- package/src/config.ts +15 -0
- package/src/daily-log.ts +182 -0
- package/src/driver.ts +14 -2
- package/src/provider.ts +22 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.4.7] - 2026-09-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Daily debug log on disk, sorted by day: `~/.pi/extensions-data/estebanforge/pi-antigravity-bridge/logs/<YYYY-MM-DD>.ndjson`, one JSON record per line, 14-day retention. Built for support: when something breaks, the last days' files replay the failure (engine, session, bridge call, error) without reproducing it. Both engines and everything around them feed it: turn start/outcome with error text, driver failures (stall, abort, timeout, nonzero exit), ACP session load/new and connection exits, bridge tool calls and round-trip failures, AskAntigravity runs, `/agy` commands, ACP setup/self-heal/auth, and the login URL (query string stripped). Secret-shaped values (tokens, API keys, credentials, header blocks) are redacted; prompt text, tool arguments, and tool output never land in the log; an unwritable directory is skipped silently and retried. `/agy doctor` prints the directory.
|
|
10
|
+
- Two verbosity tiers keep the disk cost negligible: by default only the info/warn/error skeleton is written (a handful of records per turn). `AGY_DEBUG=1` (or `true`/`on`) restores the full per-event trail: spawn/exit, session load/new, unparks, recycle causes, raw bridge chatter. Turn it on to reproduce, then off.
|
|
11
|
+
- Pre-dispatch turn errors now reach the log (previously they only surfaced as a one-line pi error and were lost): driver start failures, stray tool results with no active turn, the ACP plan-mode refusal, and a miswired extension.
|
|
12
|
+
- The engine capabilities comparison table in the README: 20 rows comparing `stream-json` vs `acp` (thinking text, token usage, image/audio input, plan mode, slash-command handling, model/effort switching, process lifecycle, session resume, abort, bridge routing, edit diffs, permissions, digest and system-prompt delivery, auth, wire protocol, doctor diagnostics), peer-reviewed against the code and the live probes.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- `/agy doctor` prints the log directory with a hint to attach recent days' files when reporting issues.
|
|
17
|
+
- README, architecture module map, and the regression-test list document the logger, its support flow, and the `AGY_DEBUG` gate.
|
|
18
|
+
|
|
19
|
+
## [1.4.6] - 2026-09-04
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Tool-priority note in the system prompt block (`systemPrompt` setting, on by default): agy runs embedded in pi, so its native interactive tools (the live example was `ask_question` vs the bridge's `ask_user_question`) never reach the user. The note tells agy to always prefer the Pi Bridge tool when one covers the same purpose. The old preamble sentence that allowed "your own tools or the pi tool bridge" is gone, since that ambiguity was the cause.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `/agy auth` reports a distinct failure instead of a false "Signed in." when the server accepts the sign-in but `acp_token.json` never appears within the 5 s post-authenticate grace window (peer-review find; the poll result was never read).
|
|
28
|
+
- Test-infra: the fake ACP server's request log was lost on SIGTERM (buffered write stream + Gate D teardown), which flaked the Gate D abort test ~1-in-3 suite runs; the log now appends synchronously.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- The sign-in URL toast fences the URL with blank lines so it stays readable and copyable next to the ssh port-forward hint.
|
|
33
|
+
|
|
5
34
|
## [1.4.5] - 2026-09-04
|
|
6
35
|
|
|
7
36
|
### Added
|
|
8
37
|
|
|
9
|
-
-
|
|
38
|
+
- Tool-priority note in the system prompt block (`systemPrompt` setting, on by default): agy runs embedded in pi, so its native interactive tools (the live example was `ask_question` vs the bridge's `ask_user_question`) never reach the user. The note tells agy to always prefer the Pi Bridge tool when one covers the same purpose. The old preamble sentence that allowed "your own tools or the pi tool bridge" is gone, since that ambiguity was the cause.
|
|
39
|
+
|
|
40
|
+
- `/agy auth` subcommand: runs the antigravity-acp sign-in on demand (spawns the server, sends `authenticate`, waits for the browser round-trip to complete; minutes-scale). Signing in no longer rides the first Antigravity message, and both login-pending moments now point at `/agy auth`. The sign-in URL toast (and its ssh port-forward hint) fires during `/agy auth` exactly as it does during turns, and a second concurrent `/agy auth` fails fast instead of racing the token write.
|
|
10
41
|
|
|
11
42
|
### Changed
|
|
12
43
|
|
package/README.md
CHANGED
|
@@ -23,7 +23,30 @@ Turns run through one of two engines behind the same provider surface (`config.e
|
|
|
23
23
|
- **stream-json** (default): the persistent `agy` CLI process. The tested default; live token usage; conversation resume via `--conversation`.
|
|
24
24
|
- **acp**: Google's official ACP server (`agy_acp_server.par`), JSON-RPC 2.0 over stdio. Opt-in while it matures: the current build (RC01) ships no usage fields (token display shows zero) and no cancel (abort tears the server down and reloads it next turn). Everything else is parity-verified live - text streaming, multi-turn resume via `session/load`, bridge tools, effort switching, serialization, abort recovery - see `scripts/parity-live.mjs`.
|
|
25
25
|
|
|
26
|
-
Engine-dependent features: pi image attachments ride natively only on the ACP engine (the picker offers image attach automatically when `config.engine` is `acp`; the stream-json CLI prompt is text-only). With the optional G1 digest enabled, its delivery also differs: ACP ships it as a native `embeddedContext` resource block, stream-json prepends it to the prompt text.
|
|
26
|
+
Engine-dependent features: pi image attachments ride natively only on the ACP engine (the picker offers image attach automatically when `config.engine` is `acp`; the stream-json CLI prompt is text-only). With the optional G1 digest enabled, its delivery also differs: ACP ships it as a native `embeddedContext` resource block, stream-json prepends it to the prompt text. The `AskAntigravity` delegation tool is unaffected by `config.engine` and runs the `stream-json` CLI (`agy -p`) across both configurations.
|
|
27
|
+
|
|
28
|
+
| Capability | `stream-json` (default) | `acp` |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| Show thinking text | No (token count only, floor 64, no text body) | Yes (streams thought text via `agent_thought_chunk`; sparse on RC01 where reasoning often arrives in message text) |
|
|
31
|
+
| Live token usage | Yes (live metrics from CLI step events) | No (absent in RC01, displays zero tokens) |
|
|
32
|
+
| Image prompt input | No (CLI prompt is text-only; images dropped) | Yes (native image blocks forwarded to server) |
|
|
33
|
+
| Audio prompt input | No (dropped) | Protocol advertised (`promptCapabilities.audio: true`) |
|
|
34
|
+
| Review-only plan mode | Yes (`--mode plan` review-only via `/agy mode plan`) | No (RC01 modes are permission levels; plan mode refused) |
|
|
35
|
+
| Leading slash commands in prompt | Disabled via `--disable-slash-commands` (sent as plain text) | Server intercepts recognized commands (e.g. `/plan`) and executes them under the active policy |
|
|
36
|
+
| Dynamic model / effort switch | Recycles process on model or effort change | Dynamic per-turn via `session/set_config_option` (no restart) |
|
|
37
|
+
| Process lifecycle | 1 persistent `agy` process per provider; recycles on drift | 1 persistent server process hosting N sessions concurrently |
|
|
38
|
+
| Session resume & persistence | Client-side map in `sessions.json` via `--conversation <id>` | Server-side session store via `session/load` and `session/new` |
|
|
39
|
+
| Turn cancel / abort | Kills process group; in-flight turn terminates | Teardown, kill, and auto-reload on RC01 (-32601 fallback) |
|
|
40
|
+
| MCP tool bridge routing | Injected filesystem config via `--add-dir` | Direct `mcpServers` param in `session/new` and `session/load` |
|
|
41
|
+
| Tool execution & visibility | Native re-exec (read-only) + wrapper replay (mutating) | Server executes tools natively; events stream with content |
|
|
42
|
+
| Inline file edit diffs | Sourced from git working tree in thinking block | Sourced from `tool_call content[]` or disk vs git HEAD |
|
|
43
|
+
| Permission handling | `--dangerously-skip-permissions` (unattended CLI requirement) | Protocol-native `session/request_permission` (auto-approve when `skipPermissions` is on; auto-deny when off) |
|
|
44
|
+
| Context digest delivery (G1) | Prepend plain text inline in prompt | Native `embeddedContext` resource block |
|
|
45
|
+
| System prompt delivery (G10) | Prepend to first prompt of conversation | Prepend to first prompt of conversation |
|
|
46
|
+
| Authentication methods | Inherits existing `agy` CLI OAuth state | 4 methods: `oauth-personal`, `oauth-business`, `gemini-api-key`, `agent-platform` |
|
|
47
|
+
| Wire protocol | Undocumented CLI NDJSON stream format | Versioned JSON-RPC 2.0 over stdio (`protocolVersion: 1`) |
|
|
48
|
+
| Diagnostics (`/agy doctor`) | Child PID, state, process spawns, recycles, queue stats | Server version, agentInfo, session counts, reconnect count, cancel support |
|
|
49
|
+
| Integration channel | Spawns internal CLI stream-json dialect | Official Google first-party ACP server binary |
|
|
27
50
|
|
|
28
51
|
Switch with `/agy engine acp|stream-json` (takes effect on restart). Setup is automatic: switching to `acp` installs Google's official ACP server binary from the [antigravity-acp registry entry](https://github.com/agentclientprotocol/registry) (`~/.local/opt/agy-acp/<build>/` + a `current` symlink, zip sha256 recorded; layout and pinning in [docs/ACP-ADOPTION-PLAN.md](docs/ACP-ADOPTION-PLAN.md)) and prepares the login. The login is your Antigravity subscription: the same account and plan you use for the Antigravity CLI (`agy`). Sign in explicitly with `/agy auth` (engine `acp` selected): it opens the Google login in your browser and completes when you finish it. If no browser is available (an SSH session on a remote machine), pi shows the sign-in URL to copy, plus the ssh port-forward command for the login redirect. It is no different from logging into the CLI; the server just keeps its own token file on your machine, like any Google tool, and this extension never sees your credentials. If you also export `GEMINI_API_KEY`, it is ignored: the server uses the auth type in settings.json, and setup always writes `oauth-personal`. A session start self-heals the same way, silently when everything is ready. Manual instructions (`/agy auth-manual`) surface only when a step fails. Sessions are engine-scoped, so switching engines never crosses conversations.
|
|
29
52
|
|
|
@@ -107,7 +130,7 @@ If `agy models` fails at load (binary missing, auth not done, network stall), a
|
|
|
107
130
|
| `askTool` | `on` (register the AskAntigravity delegation tool), `off` (no delegation tool; provider and models only) | `on` |
|
|
108
131
|
| `bridgeTools` | `none` (bridge off), `all` (every non-builtin tool, incl. other `Ask*` delegations), `mcp` (pi-mcp-adapter tools + skills bridge only) | `all` |
|
|
109
132
|
| `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` |
|
|
110
|
-
| `systemPrompt` | `on` (prepend 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) or `off` (agy-native behavior) | `on` |
|
|
133
|
+
| `systemPrompt` | `on` (prepend 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) or `off` (agy-native behavior) | `on` |
|
|
111
134
|
|
|
112
135
|
Env overrides: `AGY_BRIDGE_TOOLS`, `AGY_ASK_TOOL`, `AGY_DIGEST`, `AGY_SYSTEM_PROMPT`. Env wins over the file, so while `AGY_DIGEST` or `AGY_SYSTEM_PROMPT` is set, the matching `/agy digest` or `/agy system-prompt` toggle persists a value that never takes effect.
|
|
113
136
|
|
|
@@ -120,7 +143,7 @@ The `activate_skill` catalog mirrors pi's directory-based skill discovery: the t
|
|
|
120
143
|
```
|
|
121
144
|
/agy status, or open the full settings picker (TUI)
|
|
122
145
|
/agy status print current settings + session counts
|
|
123
|
-
/agy doctor bridge state, driver counters, bridge port, last lifecycle events
|
|
146
|
+
/agy doctor bridge state, driver counters, bridge port, last lifecycle events, log dir
|
|
124
147
|
/agy auth run the antigravity-acp sign-in now (engine acp): opens the Google login in your browser, shows the URL when no browser opens
|
|
125
148
|
/agy mode plan review-only: agy plans but writes nothing
|
|
126
149
|
/agy mode accept-edits agy applies edits directly (default)
|
|
@@ -129,7 +152,7 @@ The `activate_skill` catalog mirrors pi's directory-based skill discovery: the t
|
|
|
129
152
|
/agy model flash|pro|gemini fallback model for the AskAntigravity tool; callers may override per call
|
|
130
153
|
/agy thinking low|medium|high fallback thinking tier for the AskAntigravity tool; callers may override per call
|
|
131
154
|
/agy digest on|off inject pi-side context into agy prompts (default off; see table above)
|
|
132
|
-
/agy system-prompt on|off send pi's system prompt + AGENTS.md to new agy conversations (default on)
|
|
155
|
+
/agy system-prompt on|off send pi's system prompt + AGENTS.md + the Pi Bridge tool-priority note to new agy conversations (default on)
|
|
133
156
|
/agy bridge all|mcp|none which pi tools the MCP bridge exposes to agy (default all; none = bridge off)
|
|
134
157
|
/agy acp-bin <path|auto> point the ACP engine at a specific server binary (auto = setup installs, or AGY_ACP_BIN; applies on the next ACP turn)
|
|
135
158
|
/agy engine acp|stream-json switch the turn engine (restart to apply; default stream-json; acp runs self-service setup: binary install + auth bootstrap)
|
|
@@ -163,6 +186,26 @@ For isolation when running any agent that executes commands without a confirmati
|
|
|
163
186
|
| `AGY_SKIP_PERMISSIONS` | `1`/`true` (default) to pass `--dangerously-skip-permissions` so commands don't hang on an unanswerable prompt in `-p` mode. `0`/`false` to prompt (hangs any `run_command` non-interactively). Wins over the config file. |
|
|
164
187
|
| `AGY_DEFAULT_MODEL` | Default model alias for the `AskAntigravity` tool (`flash`/`pro`/`gemini`, or a tier/version qualifier). Wins over the config file. |
|
|
165
188
|
| `AGY_DEFAULT_THINKING` | Default thinking tier for the `AskAntigravity` tool: `low`/`medium`/`high`. Anything else falls back to `medium`. Wins over the config file. |
|
|
189
|
+
| `AGY_DEBUG` | `1`/`true`/`on` writes verbose debug records to the daily log (driver lifecycle, raw bridge traffic). Default off: only the light info/warn/error stream. |
|
|
190
|
+
|
|
191
|
+
## Debug logs
|
|
192
|
+
|
|
193
|
+
The extension keeps a daily log on your machine, sorted by day:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
~/.pi/extensions-data/estebanforge/pi-antigravity-bridge/logs/<YYYY-MM-DD>.ndjson
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
One JSON record per line. Two verbosity tiers keep the disk cost negligible for regular users: by default only `info`/`warn`/`error` records land on disk, which is the useful skeleton: turn starts and outcomes with error text (both engines), bridge tool calls and round-trip failures, `AskAntigravity` runs, `/agy` commands, ACP setup/self-heal, auth URLs, and every driver failure (stall, abort, timeout, nonzero exit). Set `AGY_DEBUG=1` before reproducing a problem for the full trail: per-event driver lifecycle (spawn, exit, session load/new, unparks), list-tools traffic, recycle causes, and the raw bridge chatter. `/agy doctor` prints the log directory.
|
|
200
|
+
|
|
201
|
+
Notes:
|
|
202
|
+
|
|
203
|
+
- Retention: 14 days. Older files are pruned automatically.
|
|
204
|
+
- Privacy: prompt text, tool arguments, and tool output never land in the log. Secret-shaped values (tokens, API keys, credentials, header blocks) are redacted, and long strings are truncated. The `auth-url` record strips the login URL's query string.
|
|
205
|
+
- Logging never throws: an unwritable directory is skipped silently and retried on the next record.
|
|
206
|
+
- SSD wear: default volume is a handful of records per turn. Verbose mode (`AGY_DEBUG=1`) writes more; turn it off after reproducing.
|
|
207
|
+
|
|
208
|
+
When you report an issue, attach the last day or two of files from that directory. For anything that needs reproduction, run with `AGY_DEBUG=1` once and attach that day's file. They usually contain the exact failure sequence (engine, session, bridge call, error) with no need to guess.
|
|
166
209
|
|
|
167
210
|
## Development
|
|
168
211
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -24,6 +24,7 @@ src/discovery.ts conversation-id binding for the AskAntigravity one-shot to
|
|
|
24
24
|
src/models.ts agy models -> pi Model projection (full catalog, per-model effort)
|
|
25
25
|
src/sessions.ts atomic JSON store: pi session -> agy conversation + watermark
|
|
26
26
|
src/config.ts persisted runtime config (engine + acp block, bridgeTools, digest, mode, permissions, model/thinking defaults)
|
|
27
|
+
src/daily-log.ts daily NDJSON support log (one file per day, 14-day retention, secret redaction, AGY_DEBUG verbose gate); fed by both drivers, the bridge, round-trips, /agy, and ask-tool
|
|
27
28
|
src/ask-tool.ts the AskAntigravity one-shot delegation tool (model/thinking defaults)
|
|
28
29
|
src/mcp-server.ts MCP tool bridge server: ferries tools/list + tools/call; calls park in the provider round-trip
|
|
29
30
|
src/diff-render.ts stream-json: render agy's file edits as git diffs in pi's thinking stream; formatInlineDiff (no git) renders ACP's native diffs
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -85,6 +85,7 @@ Most "stuck" reports trace to one of:
|
|
|
85
85
|
- `tests/acp-events.test.ts` - ACP session/update mapping onto pi activities (text, thought, tool cards) and the session/load replay suppression.
|
|
86
86
|
- `tests/acp-driver.test.ts` - the ACP driver over the fake server (`tests/helpers/fake-acp-server.mjs`, scenario-selected): happy flow, load-replay, permission auto-answer, Gate D abort (cancel probe, teardown, `cancelSupported` memory), the stale-exit race (a killed connection's late exit must not fail its replacement - `ACP_FAKE_SLOW_DEATH_MS`), auth errors, park/kickIdle timer pause with remaining budget.
|
|
87
87
|
- `tests/acp-config.test.ts` - engine selection narrowing (`AGY_ENGINE`/`config.engine`), acp block parsing.
|
|
88
|
+
- `tests/daily-log.test.ts` - the support log: day rotation, retention cutoff boundary, secret redaction (incl. header blocks), the 4 KB record cap, never-throw on a broken dir, and the two-tier gate (debug records dropped unless `AGY_DEBUG`).
|
|
88
89
|
|
|
89
90
|
## Module map
|
|
90
91
|
|
package/extensions/index.ts
CHANGED
|
@@ -43,8 +43,9 @@ import { AcpDriver } from "../src/acp/driver.js";
|
|
|
43
43
|
import { runAcpAuth } from "../src/acp/auth.js";
|
|
44
44
|
import { setupAuthUrlCapture } from "../src/acp/browser-capture.js";
|
|
45
45
|
import { ensureAcpReady, inspectAcpSetup } from "../src/acp/setup.js";
|
|
46
|
-
import type { TurnDriver } from "../src/driver-types.js";
|
|
47
|
-
import { CONFIG_PATH, loadConfig, saveConfig, type AgyMode, type BridgeTools, type Engine, type ThinkingTier } from "../src/config.js";
|
|
46
|
+
import type { TurnDriver, TurnOutcome } from "../src/driver-types.js";
|
|
47
|
+
import { CONFIG_PATH, loadConfig, logsDir, saveConfig, type AgyMode, type BridgeTools, type Engine, type ThinkingTier } from "../src/config.js";
|
|
48
|
+
import { createDailyLogger, type DailyLogger } from "../src/daily-log.js";
|
|
48
49
|
import { registerAskAntigravityTool, toolModelsFromRaw } from "../src/ask-tool.js";
|
|
49
50
|
import { startMcpServer, TOKEN_HEADER, type McpServerHandle } from "../src/mcp-server.js";
|
|
50
51
|
import {
|
|
@@ -103,6 +104,17 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
103
104
|
const modelInput: Array<"text" | "image"> = engine === "acp" ? ["text", "image"] : ["text"];
|
|
104
105
|
const models = entries.map((e) => toPiModel(e, modelInput));
|
|
105
106
|
|
|
107
|
+
// Daily file log: every sink below feeds ~/.pi/extensions-data/
|
|
108
|
+
// estebanforge/pi-antigravity-bridge/logs/<YYYY-MM-DD>.ndjson (see
|
|
109
|
+
// src/daily-log.ts). Fire-and-forget, secrets redacted, old days pruned.
|
|
110
|
+
// Support flow: "attach the last days' files from that dir".
|
|
111
|
+
const fileLog = createDailyLogger({ dir: logsDir() });
|
|
112
|
+
fileLog.log(
|
|
113
|
+
"extension-load",
|
|
114
|
+
{ engine, models: models.length, fallback: usingFallback, bridge: loadConfig().bridgeTools, askTool: loadConfig().askTool },
|
|
115
|
+
"info",
|
|
116
|
+
);
|
|
117
|
+
|
|
106
118
|
const store = new SessionStore();
|
|
107
119
|
// MCP bridge handle, declared early: the ACP engine reads the bridge port
|
|
108
120
|
// at session/new / session/load time.
|
|
@@ -136,14 +148,55 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
136
148
|
"unsupported-server-request",
|
|
137
149
|
]);
|
|
138
150
|
const legacyDriver = new AgyDriver();
|
|
151
|
+
// Mirror the legacy driver's lifecycle ring into the daily file log
|
|
152
|
+
// (spawn/exit/abort/stall/recycle). The ACP driver reaches the same file
|
|
153
|
+
// through acpLog below.
|
|
154
|
+
legacyDriver.log = (msg, data) => {
|
|
155
|
+
// Level classification mirrors acpLog's failure set: stalls, aborts,
|
|
156
|
+
// timeouts and nonzero exits are the "what broke" greps (warn);
|
|
157
|
+
// turn-start is the per-turn skeleton (info); everything else is
|
|
158
|
+
// verbose-only (debug, needs AGY_DEBUG).
|
|
159
|
+
const failed =
|
|
160
|
+
msg.startsWith("timeout:") ||
|
|
161
|
+
msg.startsWith("stall:") ||
|
|
162
|
+
msg.startsWith("abort:") ||
|
|
163
|
+
(msg.startsWith("exit:") && msg !== "exit:0");
|
|
164
|
+
const level = failed ? "warn" : msg === "turn-start" ? "info" : "debug";
|
|
165
|
+
fileLog.log(msg, data, level);
|
|
166
|
+
};
|
|
139
167
|
// Shared ACP log sink (driver turns AND /agy auth): the login URL event
|
|
140
168
|
// toasts so SSH users can copy it; genuine failures reach stderr.
|
|
141
169
|
const acpLog = (msg: string, data?: unknown): void => {
|
|
170
|
+
// The daily file log gets EVERY driver event (auth-url stripped of its
|
|
171
|
+
// query string - it carries one-time login state); the filters below
|
|
172
|
+
// only decide what reaches the user.
|
|
173
|
+
const fileData =
|
|
174
|
+
msg === "auth-url"
|
|
175
|
+
? { port: (data as { port?: number | null } | undefined)?.port ?? null, url: String((data as { url?: string } | undefined)?.url ?? "").split("?")[0] }
|
|
176
|
+
: data;
|
|
177
|
+
// Failures warn; turn-start + auth-url are the always-on skeleton;
|
|
178
|
+
// routine per-event lifecycle (spawn, session-load, unparked, ...) is
|
|
179
|
+
// verbose-only.
|
|
180
|
+
const level = acpFailures.has(msg)
|
|
181
|
+
? "warn"
|
|
182
|
+
: msg === "turn-start" || msg === "auth-url"
|
|
183
|
+
? "info"
|
|
184
|
+
: "debug";
|
|
185
|
+
fileLog.log(msg, fileData, level);
|
|
142
186
|
if (msg === "auth-url") {
|
|
143
187
|
const { url, port } = (data ?? {}) as { url?: string; port?: number | null };
|
|
144
188
|
if (!url) return;
|
|
145
|
-
|
|
146
|
-
|
|
189
|
+
// Blank lines fence the URL off from the rest of the text: it is long
|
|
190
|
+
// and wraps, so separation keeps it readable and copyable.
|
|
191
|
+
const parts = ["Google sign-in URL for the ACP engine:", "", url];
|
|
192
|
+
if (port) {
|
|
193
|
+
parts.push(
|
|
194
|
+
"",
|
|
195
|
+
"SSH session? Forward the port on your machine first:",
|
|
196
|
+
` ssh -N -L ${port}:127.0.0.1:${port} <user@host>`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const text = parts.join("\n");
|
|
147
200
|
if (activeUi) activeUi.notify(text, "warning");
|
|
148
201
|
else console.error(`[antigravity-bridge acp] ${text}`);
|
|
149
202
|
return;
|
|
@@ -181,7 +234,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
181
234
|
// The no-patch pi-tool round-trip store: the MCP bridge parks calls here;
|
|
182
235
|
// the provider emits them as real pi toolUse turns and completes them from
|
|
183
236
|
// the next call's toolResult.
|
|
184
|
-
const roundTrips = new ToolRoundTrips(activeDriver);
|
|
237
|
+
const roundTrips = new ToolRoundTrips(activeDriver, (s, d) => fileLog.log(s, d, s === "round-trip-fail" ? "warn" : "debug"));
|
|
185
238
|
const replay = new WrapperReplay();
|
|
186
239
|
// Native re-exec only emits for builtins actually active in the session;
|
|
187
240
|
// anything else (or an unknown name) falls back to the wrapper card.
|
|
@@ -195,7 +248,18 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
195
248
|
};
|
|
196
249
|
// A settled turn cannot answer its parked calls; the driver never sees
|
|
197
250
|
// ToolRoundTrips, so the provider bridges the two here (both engines).
|
|
198
|
-
|
|
251
|
+
// The file log records the outcome first: ERROR/aborted turns are the
|
|
252
|
+
// single most useful support signal.
|
|
253
|
+
const onTurnEnd = (outcome: TurnOutcome) => {
|
|
254
|
+
fileLog.log(
|
|
255
|
+
"turn-end",
|
|
256
|
+
// Error text can embed the child's stderr tail; cap it in line with
|
|
257
|
+
// the ACP driver's 200-char stderr slices.
|
|
258
|
+
{ status: outcome.status, error: outcome.error?.slice(0, 500), aborted: outcome.aborted },
|
|
259
|
+
outcome.status === "OK" ? "info" : "warn",
|
|
260
|
+
);
|
|
261
|
+
roundTrips.failAll("antigravity turn ended with an unresolved pi tool call");
|
|
262
|
+
};
|
|
199
263
|
legacyDriver.onTurnEnd = onTurnEnd;
|
|
200
264
|
acpDriver.onTurnEnd = onTurnEnd;
|
|
201
265
|
const streamSimple = createStreamSimple({
|
|
@@ -207,6 +271,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
207
271
|
replay,
|
|
208
272
|
nativeActive,
|
|
209
273
|
engine,
|
|
274
|
+
log: fileLog.log.bind(fileLog),
|
|
210
275
|
});
|
|
211
276
|
|
|
212
277
|
pi.registerProvider("antigravity", {
|
|
@@ -237,6 +302,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
237
302
|
engine,
|
|
238
303
|
getMcpPort: () => mcpHandle?.port ?? null,
|
|
239
304
|
acpLog,
|
|
305
|
+
fileLog,
|
|
240
306
|
authCapture: authCapture ?? null,
|
|
241
307
|
});
|
|
242
308
|
|
|
@@ -249,7 +315,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
249
315
|
// Note: the active flag below is set regardless of askTool, so
|
|
250
316
|
// pi-ask-antigravity keeps deferring even then: off means NO delegation
|
|
251
317
|
// tool from either package, not a fallback to pi-ask-antigravity.
|
|
252
|
-
if (loadConfig().askTool) await registerAskAntigravityTool(pi, toolModels);
|
|
318
|
+
if (loadConfig().askTool) await registerAskAntigravityTool(pi, toolModels, fileLog.log.bind(fileLog));
|
|
253
319
|
|
|
254
320
|
// Display-only wrapper tool: the provider emits mutating agy steps as
|
|
255
321
|
// toolCalls against it (never re-executed - execute() replays the output
|
|
@@ -300,6 +366,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
300
366
|
if (engine === "acp" && !acpSelfHealRan) {
|
|
301
367
|
acpSelfHealRan = true;
|
|
302
368
|
void ensureAcpReady({ configBin: loadConfig().acp.bin }).then((status) => {
|
|
369
|
+
fileLog.log(
|
|
370
|
+
"acp-self-heal",
|
|
371
|
+
status.ok
|
|
372
|
+
? { ok: true, binarySource: status.binarySource, needsLogin: status.needsLogin }
|
|
373
|
+
: { ok: false, error: status.error },
|
|
374
|
+
status.ok ? "info" : "warn",
|
|
375
|
+
);
|
|
303
376
|
if (status.ok) {
|
|
304
377
|
if (status.binarySource === "installed" || status.binarySource === "existing") {
|
|
305
378
|
saveConfig({ acp: { bin: status.bin, permissions: loadConfig().acp.permissions } });
|
|
@@ -323,17 +396,28 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
323
396
|
// warning toast (ctx.ui.notify, ephemeral) or stderr when headless.
|
|
324
397
|
// Per-turn success events (list-tools / call-tool) stay silent.
|
|
325
398
|
const mcpLog = (s: string, d?: unknown) => {
|
|
399
|
+
const failures = new Set([
|
|
400
|
+
"http-error", "bridge-config-write-failed", "call-tool-fail",
|
|
401
|
+
"transport-error", "handleRequest-error", "request-error",
|
|
402
|
+
"request-handler-error", "unauthorized",
|
|
403
|
+
]);
|
|
404
|
+
// Daily file log gets every bridge event (call-tool/list-tools
|
|
405
|
+
// traffic included - it is how a parked round-trip is traced); the
|
|
406
|
+
// filters below only decide what reaches the user. Bridge calls
|
|
407
|
+
// start/end at info (one record per tool call, the fragile-path
|
|
408
|
+
// skeleton); list-tools and startup chatter stay verbose.
|
|
409
|
+
const level = failures.has(s)
|
|
410
|
+
? "warn"
|
|
411
|
+
: s === "call-tool" || s === "call-tool-ok"
|
|
412
|
+
? "info"
|
|
413
|
+
: "debug";
|
|
414
|
+
fileLog.log(s, d, level);
|
|
326
415
|
// Routine abort traffic: failAll fires on turn end / session shutdown
|
|
327
416
|
// and the bridge answers every parked call with an error. Not a fault.
|
|
328
417
|
if (s === "call-tool-fail") {
|
|
329
418
|
const detail = (d as { msg?: string } | undefined)?.msg ?? "";
|
|
330
419
|
if (detail.includes("unresolved pi tool call") || detail.includes("session shut down")) return;
|
|
331
420
|
}
|
|
332
|
-
const failures = new Set([
|
|
333
|
-
"http-error", "bridge-config-write-failed", "call-tool-fail",
|
|
334
|
-
"transport-error", "handleRequest-error", "request-error",
|
|
335
|
-
"request-handler-error", "unauthorized",
|
|
336
|
-
]);
|
|
337
421
|
if (!failures.has(s)) return;
|
|
338
422
|
const msg = `[antigravity-bridge mcp] ${s}${d !== undefined ? " " + JSON.stringify(d) : ""}`;
|
|
339
423
|
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
@@ -440,6 +524,8 @@ interface AgyCommandCtx {
|
|
|
440
524
|
getMcpPort: () => number | null;
|
|
441
525
|
/** Shared ACP log sink (login URL surfacing + failure events). */
|
|
442
526
|
acpLog: (msg: string, data?: unknown) => void;
|
|
527
|
+
/** Daily file logger (src/daily-log.ts); command + doctor surfacing. */
|
|
528
|
+
fileLog: DailyLogger;
|
|
443
529
|
/** BROWSER-capture handles; null when unavailable (Windows, unwritable
|
|
444
530
|
* data dir). /agy auth passes them to the sign-in process. */
|
|
445
531
|
authCapture: { browserEnv: Record<string, string>; file: string } | null;
|
|
@@ -493,6 +579,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
493
579
|
const mode = cmdCtx.mode;
|
|
494
580
|
const sub = (args ?? "").trim().split(/\s+/)[0]?.toLowerCase();
|
|
495
581
|
const val = (args ?? "").trim().split(/\s+/)[1]?.toLowerCase();
|
|
582
|
+
ctx.fileLog.log("agy-command", { args }, "info");
|
|
496
583
|
|
|
497
584
|
// Direct subcommands work everywhere (headless + TUI).
|
|
498
585
|
if (sub === "clear") {
|
|
@@ -539,6 +626,13 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
539
626
|
configBin: loadConfig().acp.bin,
|
|
540
627
|
onProgress: (m) => ui?.notify(m, "info"),
|
|
541
628
|
});
|
|
629
|
+
ctx.fileLog.log(
|
|
630
|
+
"acp-setup",
|
|
631
|
+
status.ok
|
|
632
|
+
? { ok: true, binarySource: status.binarySource, needsLogin: status.needsLogin }
|
|
633
|
+
: { ok: false, error: status.error },
|
|
634
|
+
status.ok ? "info" : "warn",
|
|
635
|
+
);
|
|
542
636
|
if (!status.ok) {
|
|
543
637
|
ui?.notify(`ACP auto-setup failed (${status.error}).\n${status.manual}`, "warning");
|
|
544
638
|
return;
|
|
@@ -567,6 +661,13 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
567
661
|
}
|
|
568
662
|
ui?.notify("Preparing the ACP server (binary + auth settings)…", "info");
|
|
569
663
|
const status = await ensureAcpReady({ configBin: loadConfig().acp.bin, onProgress: (m) => ui?.notify(m, "info") });
|
|
664
|
+
ctx.fileLog.log(
|
|
665
|
+
"acp-setup",
|
|
666
|
+
status.ok
|
|
667
|
+
? { ok: true, binarySource: status.binarySource, needsLogin: status.needsLogin }
|
|
668
|
+
: { ok: false, error: status.error },
|
|
669
|
+
status.ok ? "info" : "warn",
|
|
670
|
+
);
|
|
570
671
|
if (!status.ok) {
|
|
571
672
|
ui?.notify(`ACP auto-setup failed (${status.error}).\n${status.manual}`, "warning");
|
|
572
673
|
return;
|
|
@@ -586,6 +687,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
586
687
|
authUrlFile: ctx.authCapture?.file,
|
|
587
688
|
log: ctx.acpLog,
|
|
588
689
|
});
|
|
690
|
+
ctx.fileLog.log("acp-auth", r.ok ? { ok: true } : { ok: false, error: r.error }, r.ok ? "info" : "warn");
|
|
589
691
|
if (r.ok) {
|
|
590
692
|
ui?.notify("Signed in. The ACP engine is ready; takes effect on the next pi start (or /reload).", "info");
|
|
591
693
|
} else {
|
|
@@ -656,6 +758,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
656
758
|
` sessions: ${ctx.store.size} bound`,
|
|
657
759
|
` models: ${ctx.entries.length} ${ctx.usingFallback ? "FALLBACK (agy models failed)" : "discovered"}`,
|
|
658
760
|
` config: ${CONFIG_PATH}`,
|
|
761
|
+
` logs: ${logsDir()} (attach recent days' files when reporting issues)`,
|
|
659
762
|
];
|
|
660
763
|
if (snap.engine === "acp" && snap.acp) {
|
|
661
764
|
lines.push(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.7",
|
|
4
4
|
"description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. antigravity/* models in Pi's /model picker, no-patch MCP bridge: agy runs Pi's tools. ToS safe to use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/acp/auth.ts
CHANGED
|
@@ -18,10 +18,14 @@ export interface AcpAuthOptions {
|
|
|
18
18
|
cwd?: string;
|
|
19
19
|
extraEnv?: Record<string, string>;
|
|
20
20
|
authUrlFile?: string;
|
|
21
|
+
/** Directory holding acp_token.json. Default: ~/.gemini/antigravity-acp.
|
|
22
|
+
* Tests point this at a fixture dir; production callers use the default. */
|
|
23
|
+
tokenDir?: string;
|
|
21
24
|
/** The server is expected to have written acp_token.json by the time
|
|
22
25
|
* authenticate replies, but write-after-reply is unverified; after a
|
|
23
26
|
* successful reply the run polls for the token up to this long before
|
|
24
|
-
* SIGTERM, so a laggard write is never killed mid-flight.
|
|
27
|
+
* SIGTERM, so a laggard write is never killed mid-flight. A token that
|
|
28
|
+
* never appears is a distinct failure, not silent success. */
|
|
25
29
|
tokenGraceMs?: number;
|
|
26
30
|
timeoutMs?: number;
|
|
27
31
|
log?: (msg: string, data?: unknown) => void;
|
|
@@ -53,7 +57,13 @@ export function runAcpAuth(opts: AcpAuthOptions): Promise<AcpAuthResult> {
|
|
|
53
57
|
await conn.start();
|
|
54
58
|
await conn.request("authenticate", { methodId: "oauth-personal" }, opts.timeoutMs ?? AUTH_TIMEOUT_MS);
|
|
55
59
|
const deadline = Date.now() + grace;
|
|
56
|
-
while (!hasAcpToken() && Date.now() < deadline) await sleep(200);
|
|
60
|
+
while (!hasAcpToken(opts.tokenDir) && Date.now() < deadline) await sleep(200);
|
|
61
|
+
if (!hasAcpToken(opts.tokenDir)) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
error: `the server accepted the sign-in, but no token file appeared in ${opts.tokenDir ?? "~/.gemini/antigravity-acp"} within ${Math.round(grace / 1000)}s`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
57
67
|
return { ok: true };
|
|
58
68
|
} catch (err) {
|
|
59
69
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
package/src/acp/driver.ts
CHANGED
|
@@ -197,6 +197,14 @@ export class AcpDriver implements TurnDriver {
|
|
|
197
197
|
this.#active = turn;
|
|
198
198
|
this.#state = "running";
|
|
199
199
|
this.#stats.turns += 1;
|
|
200
|
+
this.#log("turn-start", {
|
|
201
|
+
model: request.model,
|
|
202
|
+
effort: request.effort,
|
|
203
|
+
mode: request.mode,
|
|
204
|
+
conversation: request.conversationId ?? null,
|
|
205
|
+
images: request.images?.length ?? 0,
|
|
206
|
+
contextBlock: request.contextBlock ? true : undefined,
|
|
207
|
+
});
|
|
200
208
|
|
|
201
209
|
// Abort wiring first: a kill during session setup must still settle the
|
|
202
210
|
// turn (Gate D teardown applies from the first request).
|
package/src/ask-tool.ts
CHANGED
|
@@ -266,6 +266,10 @@ const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
|
266
266
|
export async function registerAskAntigravityTool(
|
|
267
267
|
pi: ExtensionAPI,
|
|
268
268
|
entries: ModelEntry[],
|
|
269
|
+
/** Daily file log sink (src/daily-log.ts). Records lifecycle, never
|
|
270
|
+
* prompt text. Level matches DailyLogger: debug is the AGY_DEBUG-only
|
|
271
|
+
* verbose tier; info/warn/error always land. */
|
|
272
|
+
log?: (event: string, data?: unknown, level?: "debug" | "info" | "warn" | "error") => void,
|
|
269
273
|
): Promise<void> {
|
|
270
274
|
pi.registerTool({
|
|
271
275
|
name: "AskAntigravity",
|
|
@@ -469,6 +473,11 @@ export async function registerAskAntigravityTool(
|
|
|
469
473
|
: finalPrompt;
|
|
470
474
|
|
|
471
475
|
const args: string[] = ["--add-dir", cwd];
|
|
476
|
+
log?.(
|
|
477
|
+
"ask-start",
|
|
478
|
+
{ model: resolved.model, thinking: resolved.effort ?? config.defaultThinking, mode, digest: useDigest, continue: isContinuation, timeoutMin },
|
|
479
|
+
"info",
|
|
480
|
+
);
|
|
472
481
|
const extra = extraArgs();
|
|
473
482
|
if (extra.length) args.push(...extra);
|
|
474
483
|
if (resolved.model) args.push("--model", resolved.model);
|
|
@@ -625,6 +634,11 @@ export async function registerAskAntigravityTool(
|
|
|
625
634
|
details.aborted = outcome.aborted;
|
|
626
635
|
details.timedOut = outcome.timedOut;
|
|
627
636
|
details.durationMs = Date.now() - start;
|
|
637
|
+
log?.(
|
|
638
|
+
"ask-end",
|
|
639
|
+
{ exitCode: outcome.exitCode, aborted: outcome.aborted, timedOut: outcome.timedOut, durationMs: details.durationMs, conversationId: details.conversationId },
|
|
640
|
+
outcome.exitCode !== 0 || outcome.aborted || outcome.timedOut ? "warn" : "info",
|
|
641
|
+
);
|
|
628
642
|
|
|
629
643
|
if (!isContinuation && !details.conversationId && snapshot) {
|
|
630
644
|
for (let attempt = 0; attempt < DISCOVERY_POLL_ATTEMPTS; attempt++) {
|
|
@@ -680,6 +694,7 @@ export async function registerAskAntigravityTool(
|
|
|
680
694
|
if (statusInterval) clearInterval(statusInterval);
|
|
681
695
|
details.durationMs = Date.now() - start;
|
|
682
696
|
const msg = err instanceof Error ? err.message : String(err);
|
|
697
|
+
log?.("ask-fail", { error: msg, durationMs: details.durationMs }, "error");
|
|
683
698
|
return { content: [{ type: "text", text: `failed to run agy: ${msg}` }], details };
|
|
684
699
|
}
|
|
685
700
|
finally {
|
package/src/config.ts
CHANGED
|
@@ -22,6 +22,21 @@ const CONFIG_PATH = path.join(
|
|
|
22
22
|
"config.json",
|
|
23
23
|
);
|
|
24
24
|
|
|
25
|
+
/** Daily debug logs land here, sorted by day, for post-mortems and user
|
|
26
|
+
* support (see src/daily-log.ts). Same homedir convention as CONFIG_PATH;
|
|
27
|
+
* lives under the scoped extensions-data convention shared with other
|
|
28
|
+
* EstebanForge extensions. */
|
|
29
|
+
export function logsDir(): string {
|
|
30
|
+
return path.join(
|
|
31
|
+
os.homedir(),
|
|
32
|
+
".pi",
|
|
33
|
+
"extensions-data",
|
|
34
|
+
"estebanforge",
|
|
35
|
+
"pi-antigravity-bridge",
|
|
36
|
+
"logs",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
/** Which turn engine drives turns. "stream-json" is the tested default;
|
|
26
41
|
* "acp" is the official-server engine, opt-in (plan §9.5). */
|
|
27
42
|
export type Engine = "stream-json" | "acp";
|
package/src/daily-log.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Daily NDJSON debug log for support and post-mortems.
|
|
2
|
+
//
|
|
3
|
+
// One file per local day: <dir>/YYYY-MM-DD.ndjson, one JSON record per line.
|
|
4
|
+
// Everything the extension logs (driver lifecycle, bridge traffic, round
|
|
5
|
+
// trips, turn outcomes, /agy commands) lands here so a broken session can be
|
|
6
|
+
// replayed from disk instead of from a user's memory. Users attach the last
|
|
7
|
+
// days' files when reporting issues.
|
|
8
|
+
//
|
|
9
|
+
// Hard rules (learned from the pi-token-cost-ledger writer):
|
|
10
|
+
// - Never throw, never await in the hot path. Logging must never disrupt
|
|
11
|
+
// a turn; a broken/unwritable dir is swallowed.
|
|
12
|
+
// - One appendFile per record: O_APPEND keeps single-line writes atomic,
|
|
13
|
+
// so two pi tabs sharing the dir stay line-consistent.
|
|
14
|
+
// - No secrets, no prompt content: values of secret-shaped keys are
|
|
15
|
+
// redacted and long strings are truncated before they reach disk.
|
|
16
|
+
// - Retention: files older than `retentionDays` are pruned once per
|
|
17
|
+
// process, so the dir cannot grow unbounded.
|
|
18
|
+
// - Two tiers, to keep SSD wear negligible for regular users: only
|
|
19
|
+
// info/warn/error records (failures, turn/tool boundaries, commands,
|
|
20
|
+
// setup) are written by default. Full verbose trails (per-event driver
|
|
21
|
+
// lifecycle, raw bridge traffic) require AGY_DEBUG=1.
|
|
22
|
+
|
|
23
|
+
import { appendFile, mkdir, readdir, unlink } from "node:fs/promises";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
|
|
26
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
27
|
+
|
|
28
|
+
export interface DailyLoggerOptions {
|
|
29
|
+
/** Target directory (created on first write). */
|
|
30
|
+
dir: string;
|
|
31
|
+
/** Files older than this many days are pruned once per process. Default 14. */
|
|
32
|
+
retentionDays?: number;
|
|
33
|
+
/** Verbose gate. When false (default), debug-level records are dropped:
|
|
34
|
+
* only info/warn/error land on disk, the light stream regular users
|
|
35
|
+
* keep. AGY_DEBUG=1 (or this option) restores the full trail. */
|
|
36
|
+
debug?: boolean;
|
|
37
|
+
/** Injectable clock for tests. */
|
|
38
|
+
now?: () => Date;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DailyLogger {
|
|
42
|
+
log(event: string, data?: unknown, level?: LogLevel): void;
|
|
43
|
+
/** Resolves when every queued write settled (tests, shutdown). */
|
|
44
|
+
flush(): Promise<void>;
|
|
45
|
+
readonly dir: string;
|
|
46
|
+
/** Path of the file the next record lands in (doctor display). */
|
|
47
|
+
todayPath(): string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Local-date key, lexicographically sortable: 2026-02-05. */
|
|
51
|
+
function dayKey(d: Date): string {
|
|
52
|
+
const p = (n: number) => String(n).padStart(2, "0");
|
|
53
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const MAX_STRING = 2000;
|
|
57
|
+
const MAX_DEPTH = 6;
|
|
58
|
+
/** Cap for one serialized line: keeps the O_APPEND atomic-write guarantee
|
|
59
|
+
* honest for PIPE_BUF-sized writes even when many capped fields combine. */
|
|
60
|
+
const MAX_RECORD = 4000;
|
|
61
|
+
// Key-name redaction. `headers` is included wholesale: MCP server configs
|
|
62
|
+
// carry auth material under headers[] ({name, value} pairs) and the token
|
|
63
|
+
// lives in `value`, which a name-only regex would miss.
|
|
64
|
+
const SECRET_KEY = /token|secret|password|passphrase|authorization|api[-_]?key|cookie|headers/i;
|
|
65
|
+
|
|
66
|
+
/** Redact secret-shaped values and cap runaway strings before disk. */
|
|
67
|
+
function scrub(value: unknown, depth: number): unknown {
|
|
68
|
+
if (value === null || value === undefined) return value;
|
|
69
|
+
if (value instanceof Error) {
|
|
70
|
+
return { name: value.name, message: value.message, stack: scrub(value.stack, depth) };
|
|
71
|
+
}
|
|
72
|
+
if (typeof value === "string") {
|
|
73
|
+
return value.length > MAX_STRING ? value.slice(0, MAX_STRING) + "…(truncated)" : value;
|
|
74
|
+
}
|
|
75
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
76
|
+
if (depth >= MAX_DEPTH) return "(depth limit)";
|
|
77
|
+
if (Array.isArray(value)) return value.slice(0, 50).map((v) => scrub(v, depth + 1));
|
|
78
|
+
if (typeof value === "object") {
|
|
79
|
+
const out: Record<string, unknown> = {};
|
|
80
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
81
|
+
out[k] = SECRET_KEY.test(k) ? "[redacted]" : scrub(v, depth + 1);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
return String(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Matches the truthy set used across the extension's env parsing. */
|
|
89
|
+
function isEnvTruthy(v: string | undefined): boolean {
|
|
90
|
+
return v !== undefined && ["1", "true", "on"].includes(v.toLowerCase());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createDailyLogger(opts: DailyLoggerOptions): DailyLogger {
|
|
94
|
+
const dir = opts.dir;
|
|
95
|
+
const retentionDays = opts.retentionDays ?? 14;
|
|
96
|
+
const verbose = opts.debug ?? isEnvTruthy(process.env.AGY_DEBUG);
|
|
97
|
+
const now = opts.now ?? (() => new Date());
|
|
98
|
+
// Serialized write chain: keeps day-rotation (mkdir + prune) ordered
|
|
99
|
+
// ahead of the records that triggered it. Volume is lifecycle-events
|
|
100
|
+
// low, so chaining costs nothing.
|
|
101
|
+
let chain: Promise<void> = Promise.resolve();
|
|
102
|
+
let ensuredDay = "";
|
|
103
|
+
let pruned = false;
|
|
104
|
+
|
|
105
|
+
async function prune(): Promise<void> {
|
|
106
|
+
if (pruned) return;
|
|
107
|
+
pruned = true;
|
|
108
|
+
const cutoff = dayKey(new Date(now().getTime() - retentionDays * 86_400_000));
|
|
109
|
+
const names = await readdir(dir).catch(() => [] as string[]);
|
|
110
|
+
for (const name of names) {
|
|
111
|
+
if (!name.endsWith(".ndjson")) continue;
|
|
112
|
+
if (name.slice(0, 10) >= cutoff) continue;
|
|
113
|
+
await unlink(path.join(dir, name)).catch(() => {
|
|
114
|
+
/* in use or gone: keep the rest */
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function write(level: LogLevel, event: string, data: unknown): void {
|
|
120
|
+
const d = now();
|
|
121
|
+
const day = dayKey(d);
|
|
122
|
+
// Build the line outside the chain but guarded: scrub()/stringify run
|
|
123
|
+
// synchronously in log(), and a throwing getter in a future caller's
|
|
124
|
+
// data must not break the never-throw contract.
|
|
125
|
+
let line: string;
|
|
126
|
+
try {
|
|
127
|
+
const record: Record<string, unknown> = {
|
|
128
|
+
ts: d.toISOString(),
|
|
129
|
+
level,
|
|
130
|
+
event,
|
|
131
|
+
};
|
|
132
|
+
if (data !== undefined) record.data = scrub(data, 0);
|
|
133
|
+
line = JSON.stringify(record);
|
|
134
|
+
if (line.length > MAX_RECORD) {
|
|
135
|
+
record.data = `(record exceeded ${MAX_RECORD} bytes; payload dropped)`;
|
|
136
|
+
line = JSON.stringify(record);
|
|
137
|
+
}
|
|
138
|
+
line += "\n";
|
|
139
|
+
} catch {
|
|
140
|
+
line = `${JSON.stringify({ ts: d.toISOString(), level, event, data: "(unserializable)" })}\n`;
|
|
141
|
+
}
|
|
142
|
+
// Single chained task per record: an append failure retries ONCE inside
|
|
143
|
+
// the same task (day gate reset -> recursive mkdir rebuild -> append
|
|
144
|
+
// again). flush() therefore always waits past the retry, and a
|
|
145
|
+
// persistent failure costs at most two fs attempts, never a loop.
|
|
146
|
+
chain = chain.then(async () => {
|
|
147
|
+
try {
|
|
148
|
+
if (ensuredDay !== day) {
|
|
149
|
+
await mkdir(dir, { recursive: true });
|
|
150
|
+
ensuredDay = day;
|
|
151
|
+
await prune();
|
|
152
|
+
}
|
|
153
|
+
await appendFile(path.join(dir, `${day}.ndjson`), line, "utf8");
|
|
154
|
+
} catch {
|
|
155
|
+
try {
|
|
156
|
+
ensuredDay = "";
|
|
157
|
+
await mkdir(dir, { recursive: true });
|
|
158
|
+
ensuredDay = day;
|
|
159
|
+
await appendFile(path.join(dir, `${day}.ndjson`), line, "utf8");
|
|
160
|
+
} catch {
|
|
161
|
+
/* swallow: an unwritable dir must not disrupt the chat */
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
log(event, data, level = "debug") {
|
|
169
|
+
// Volume gate: debug is the verbose tier. Default installs write
|
|
170
|
+
// only info/warn/error so the disk cost stays negligible.
|
|
171
|
+
if (level === "debug" && !verbose) return;
|
|
172
|
+
write(level, event, data);
|
|
173
|
+
},
|
|
174
|
+
flush() {
|
|
175
|
+
return chain.catch(() => {});
|
|
176
|
+
},
|
|
177
|
+
dir,
|
|
178
|
+
todayPath() {
|
|
179
|
+
return path.join(dir, `${dayKey(now())}.ndjson`);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
package/src/driver.ts
CHANGED
|
@@ -137,6 +137,9 @@ export class AgyDriver implements TurnDriver {
|
|
|
137
137
|
// it ate large tool frames and could hide the result frame of a turn.
|
|
138
138
|
#stdoutBuf = "";
|
|
139
139
|
#lifecycle: string[] = [];
|
|
140
|
+
/** Optional external lifecycle sink (the extension's daily file log).
|
|
141
|
+
* Fire-and-forget: the ring buffer stays the source for /agy doctor. */
|
|
142
|
+
log?: (msg: string, data?: unknown) => void;
|
|
140
143
|
#onTurnEnd: ((outcome: TurnOutcome) => void) | undefined;
|
|
141
144
|
#stats = {
|
|
142
145
|
spawns: 0,
|
|
@@ -227,6 +230,13 @@ export class AgyDriver implements TurnDriver {
|
|
|
227
230
|
this.#active = turn;
|
|
228
231
|
this.#state = "running";
|
|
229
232
|
this.#stats.turns += 1;
|
|
233
|
+
this.#log("turn-start", {
|
|
234
|
+
model: request.model,
|
|
235
|
+
effort: request.effort,
|
|
236
|
+
mode: request.mode,
|
|
237
|
+
conversation: request.conversationId ?? null,
|
|
238
|
+
images: request.images?.length ?? 0,
|
|
239
|
+
});
|
|
230
240
|
this.#armTimers(turn);
|
|
231
241
|
|
|
232
242
|
const line = `${JSON.stringify({
|
|
@@ -628,8 +638,10 @@ export class AgyDriver implements TurnDriver {
|
|
|
628
638
|
}
|
|
629
639
|
}
|
|
630
640
|
|
|
631
|
-
#log(msg: string): void {
|
|
632
|
-
|
|
641
|
+
#log(msg: string, data?: unknown): void {
|
|
642
|
+
const line = `${nowIso()} ${msg}${data !== undefined ? ` ${JSON.stringify(data)}` : ""}`;
|
|
643
|
+
this.#lifecycle.push(line);
|
|
633
644
|
if (this.#lifecycle.length > LIFECYCLE_LIMIT) this.#lifecycle.shift();
|
|
645
|
+
this.log?.(msg, data);
|
|
634
646
|
}
|
|
635
647
|
}
|
package/src/provider.ts
CHANGED
|
@@ -108,10 +108,19 @@ const DIGEST_PREAMBLE =
|
|
|
108
108
|
// afterwards, and agy's server-side prompt cache keeps hitting.
|
|
109
109
|
|
|
110
110
|
export const SYSTEM_PROMPT_PREAMBLE =
|
|
111
|
-
"[The following is the system prompt of the pi session that spawned this conversation: operating instructions plus project context (AGENTS.md files). Apply it for this whole conversation.
|
|
111
|
+
"[The following is the system prompt of the pi session that spawned this conversation: operating instructions plus project context (AGENTS.md files). Apply it for this whole conversation.]";
|
|
112
112
|
|
|
113
113
|
export const SYSTEM_PROMPT_END = "[END SYSTEM PROMPT]";
|
|
114
114
|
|
|
115
|
+
/** Brief tool-priority note appended inside every system prompt block: agy
|
|
116
|
+
* runs embedded in pi, so its native interactive tools never reach the
|
|
117
|
+
* user. Equivalent Pi Bridge tools must win. Concrete clash observed live:
|
|
118
|
+
* agy picked its native ask_question over the bridge's ask_user_question
|
|
119
|
+
* and the question never displayed. Rides the systemPrompt gate: the note
|
|
120
|
+
* ships only when the system prompt ships. */
|
|
121
|
+
export const TOOL_PRIORITY_NOTE =
|
|
122
|
+
"[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.]";
|
|
123
|
+
|
|
115
124
|
/** Assemble the full agy prompt: system prompt block, pi-side digest, user
|
|
116
125
|
* prompt. Empty parts are dropped. Pure; exported for unit testing.
|
|
117
126
|
* Pass systemPrompt only on a fresh conversation (see runTurnDriver). */
|
|
@@ -122,7 +131,7 @@ export function buildFullPrompt(
|
|
|
122
131
|
): string {
|
|
123
132
|
const parts: string[] = [];
|
|
124
133
|
if (systemPrompt) {
|
|
125
|
-
parts.push(`${SYSTEM_PROMPT_PREAMBLE}\n\n${systemPrompt}\n\n${SYSTEM_PROMPT_END}`);
|
|
134
|
+
parts.push(`${SYSTEM_PROMPT_PREAMBLE}\n\n${systemPrompt}\n\n${TOOL_PRIORITY_NOTE}\n\n${SYSTEM_PROMPT_END}`);
|
|
126
135
|
}
|
|
127
136
|
if (digest) {
|
|
128
137
|
parts.push(`${DIGEST_PREAMBLE}\n\n${digest}`);
|
|
@@ -320,6 +329,9 @@ export interface StreamSimpleDeps {
|
|
|
320
329
|
* read decides (tests); production wiring always passes it so a
|
|
321
330
|
* mid-session config flip cannot move one side of a parked turn. */
|
|
322
331
|
engine?: "stream-json" | "acp";
|
|
332
|
+
/** Daily file log sink (src/daily-log.ts). Records pre-dispatch turn
|
|
333
|
+
* errors that never create a driver turn (and so never reach onTurnEnd). */
|
|
334
|
+
log?: (event: string, data?: unknown, level?: "debug" | "info" | "warn" | "error") => void;
|
|
323
335
|
}
|
|
324
336
|
|
|
325
337
|
/** pi thinking-effort order mirrors agy's, for clamping. */
|
|
@@ -531,6 +543,8 @@ export interface DriverDeps {
|
|
|
531
543
|
nativeActive?: (name: string) => boolean;
|
|
532
544
|
/** Active engine (config), for engine-scoped session keys. */
|
|
533
545
|
engine: "stream-json" | "acp";
|
|
546
|
+
/** Daily file log sink for pre-dispatch errors (see StreamSimpleDeps). */
|
|
547
|
+
log?: (event: string, data?: unknown, level?: "debug" | "info" | "warn" | "error") => void;
|
|
534
548
|
}
|
|
535
549
|
|
|
536
550
|
/** Map one DriverActivity onto the open pi stream. Returns "parked" when the
|
|
@@ -731,6 +745,7 @@ async function runTurnDriver(
|
|
|
731
745
|
if (isContinuation) {
|
|
732
746
|
const active = deps.driver.reentry();
|
|
733
747
|
if (!active) {
|
|
748
|
+
deps.log?.("turn-error", { reason: "tool-result-no-active-turn" }, "warn");
|
|
734
749
|
finalize(stream, blocks, "error", "tool result arrived but no antigravity turn is running");
|
|
735
750
|
return;
|
|
736
751
|
}
|
|
@@ -741,6 +756,7 @@ async function runTurnDriver(
|
|
|
741
756
|
// An image-only message (no text) is valid on the ACP engine; only fail
|
|
742
757
|
// when there is nothing at all to send.
|
|
743
758
|
if (!prompt && images.length === 0) {
|
|
759
|
+
deps.log?.("turn-error", { reason: "no-user-message" }, "debug");
|
|
744
760
|
finalize(stream, blocks, "error", "No user message to send to agy.");
|
|
745
761
|
return;
|
|
746
762
|
}
|
|
@@ -783,6 +799,7 @@ async function runTurnDriver(
|
|
|
783
799
|
});
|
|
784
800
|
} catch (err) {
|
|
785
801
|
const msg = err instanceof Error ? err.message : String(err);
|
|
802
|
+
deps.log?.("turn-error", { reason: "driver-start-failed", error: msg }, "error");
|
|
786
803
|
finalize(stream, blocks, "error", `agy failed to start: ${msg}`);
|
|
787
804
|
return;
|
|
788
805
|
}
|
|
@@ -851,6 +868,7 @@ export function createStreamSimple(
|
|
|
851
868
|
if (selected === deps.acpDriver && config.mode === "plan") {
|
|
852
869
|
const partial = newAssistant(model);
|
|
853
870
|
const blocks: BlockState = { partial, textIdx: null, thinkingIdx: null, started: false };
|
|
871
|
+
deps.log?.("turn-error", { reason: "acp-plan-refused" }, "warn");
|
|
854
872
|
finalize(stream, blocks, "error", "ACP engine has no plan mode. /agy mode accept-edits, or /agy engine stream-json.");
|
|
855
873
|
return stream;
|
|
856
874
|
}
|
|
@@ -865,12 +883,14 @@ export function createStreamSimple(
|
|
|
865
883
|
// and keying the session as @acp would store a legacy
|
|
866
884
|
// conversationId under the wrong engine scope.
|
|
867
885
|
engine: selected === deps.acpDriver ? "acp" : "stream-json",
|
|
886
|
+
log: deps.log,
|
|
868
887
|
});
|
|
869
888
|
} else {
|
|
870
889
|
// Miswired extension: no driver means no engine. Fail the turn visibly
|
|
871
890
|
// instead of silently producing an empty assistant message.
|
|
872
891
|
const partial = newAssistant(model);
|
|
873
892
|
const blocks: BlockState = { partial, textIdx: null, thinkingIdx: null, started: false };
|
|
893
|
+
deps.log?.("turn-error", { reason: "driver-not-configured" }, "warn");
|
|
874
894
|
finalize(stream, blocks, "error", "antigravity driver not configured");
|
|
875
895
|
}
|
|
876
896
|
return stream;
|