@estebanforge/pi-antigravity-bridge 1.1.2 → 1.2.4
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 +36 -0
- package/package.json +1 -1
- package/src/ask-tool.ts +231 -14
- package/src/models.ts +24 -19
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.2.4] - 2026-08-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Model, thinking tier, and mode shown next to the tool name.** The
|
|
10
|
+
`AskAntigravity` tool now renders
|
|
11
|
+
`AskAntigravity [model=gemini-3.6-flash, thinking=high]` with a prompt
|
|
12
|
+
preview while a delegation runs, plus a tidy result row
|
|
13
|
+
(`✓ AskAntigravity 12.3s`) with an expandable body. Built on pi's
|
|
14
|
+
`renderCall`/`renderResult` hooks; the values shown are the resolved
|
|
15
|
+
config defaults (model alias + tier), not just the args the caller passed.
|
|
16
|
+
`AgyDetails` gained a `thinking` field.
|
|
17
|
+
- **Opt-in full-context delegation (`includeContext`).** New boolean param
|
|
18
|
+
(default `false`, isolated one-shot unchanged). When `true`, the current pi
|
|
19
|
+
conversation is exported as resolved markdown to
|
|
20
|
+
`~/.pi/extensions-data/estebanforge/pi-antigravity-bridge/` and the prompt
|
|
21
|
+
tells agy to read it first. The run passes `--add-dir` for that folder so
|
|
22
|
+
the sandbox can read it; the temp file is removed after the run.
|
|
23
|
+
|
|
24
|
+
## [1.2.3] - 2026-08-10
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Model parsing now handles agy's real two-column output.** `agy models`
|
|
29
|
+
prints `<slug> <display label>` per line, and `--model` accepts only the
|
|
30
|
+
slug. `entriesFromRaw` (provider path) applied its slug regex to the whole
|
|
31
|
+
line, so every real line was rejected and the provider always fell back to
|
|
32
|
+
the hardcoded catalog; it now splits column 1 and requires a hyphen, which
|
|
33
|
+
also drops banner words split out of column 1. The AskAntigravity resolver
|
|
34
|
+
swallowed slug + label into `--model`, which agy rejected; it now returns a
|
|
35
|
+
`{model, effort?}` shape that sends Gemini-family bases' base slug to
|
|
36
|
+
`--model` and their tier to `--effort`, while fixed-thinking families
|
|
37
|
+
(Claude, GPT-OSS) keep the exact slug with no `--effort` (agy rejects it for
|
|
38
|
+
them). Matches the provider's collapse + clamping path. Tests rewritten to
|
|
39
|
+
the verified live `agy models` fixture.
|
|
40
|
+
|
|
5
41
|
## [1.1.2] - 2026-08-10
|
|
6
42
|
|
|
7
43
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Streaming Gemini provider for pi, built on the agy CLI. Registers antigravity/* models in pi's /model picker via SQLite polling + protobuf decode of agy's conversation DBs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/ask-tool.ts
CHANGED
|
@@ -15,6 +15,9 @@ import { spawn } from "node:child_process";
|
|
|
15
15
|
import * as fs from "node:fs";
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { buildSessionContext, getAgentDir, keyHint } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
20
|
+
import { contentText } from "@earendil-works/pi-ai";
|
|
18
21
|
import { Type } from "typebox";
|
|
19
22
|
import {
|
|
20
23
|
CONVERSATIONS_DIR,
|
|
@@ -30,6 +33,10 @@ const DEFAULT_TIMEOUT_MIN = 10;
|
|
|
30
33
|
const GRACE_AFTER_TIMEOUT_MS = 5000;
|
|
31
34
|
const STATUS_INTERVAL_MS = 1000;
|
|
32
35
|
const STATUS_TAIL_CHARS = 160;
|
|
36
|
+
|
|
37
|
+
// renderCall / renderResult preview limits (match pi-claude-bridge).
|
|
38
|
+
const PREVIEW_MAX_CHARS = 1000;
|
|
39
|
+
const PREVIEW_MAX_LINES = 6;
|
|
33
40
|
const DISCOVERY_POLL_ATTEMPTS = 5;
|
|
34
41
|
const DISCOVERY_POLL_MS = 100;
|
|
35
42
|
|
|
@@ -84,6 +91,15 @@ interface ModelEntry {
|
|
|
84
91
|
tier: ThinkingTier | null;
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
/** Argv-facing model resolution: the exact --model slug plus an optional
|
|
95
|
+
* --effort tier. Gemini bases split the tier out (the base slug alone is
|
|
96
|
+
* invalid without --effort); fixed-thinking families keep agy's exact slug
|
|
97
|
+
* and carry no effort. */
|
|
98
|
+
interface ResolvedModel {
|
|
99
|
+
model: string;
|
|
100
|
+
effort?: ThinkingTier;
|
|
101
|
+
}
|
|
102
|
+
|
|
87
103
|
// --- Version helpers -------------------------------------------------------
|
|
88
104
|
|
|
89
105
|
/** Descending numeric version compare (3.10 > 3.9, not lexical). */
|
|
@@ -110,7 +126,10 @@ function mergeCatalog(live: ModelEntry[]): ModelEntry[] {
|
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
function parseModelLine(line: string): ModelEntry | null {
|
|
113
|
-
|
|
129
|
+
// agy prints TWO columns: "<slug> <display label>". --model takes only the
|
|
130
|
+
// slug (col 1), so split it off; the label is display-only and must never
|
|
131
|
+
// reach --model. A bare-slug line (no whitespace) splits to itself.
|
|
132
|
+
const full = line.trim().split(/\s+/)[0] ?? "";
|
|
114
133
|
if (!full) return null;
|
|
115
134
|
const lower = full.toLowerCase();
|
|
116
135
|
const family: Family = lower.includes("flash")
|
|
@@ -135,21 +154,36 @@ function nearestTier(available: ThinkingTier[], preferred: ThinkingTier): Thinki
|
|
|
135
154
|
return sorted[0] ?? preferred;
|
|
136
155
|
}
|
|
137
156
|
|
|
138
|
-
/**
|
|
157
|
+
/** Build the argv-facing resolution from a picked catalog entry. Gemini bases
|
|
158
|
+
* (slugs starting "gemini-") accept a separate --effort, so split the tier
|
|
159
|
+
* suffix out of the slug: the base alone (gemini-3.6-flash) is what --model
|
|
160
|
+
* wants, and the tier goes to --effort. Fixed-thinking families keep agy's
|
|
161
|
+
* exact slug even when it carries a -medium suffix (gpt-oss-120b-medium):
|
|
162
|
+
* agy rejects --effort for them, so the suffix stays part of the slug. */
|
|
163
|
+
function toResolved(full: string, tier: ThinkingTier | null): ResolvedModel {
|
|
164
|
+
if (tier && full.toLowerCase().startsWith("gemini-")) {
|
|
165
|
+
return { model: full.replace(/-(low|medium|high)$/, ""), effort: tier };
|
|
166
|
+
}
|
|
167
|
+
return { model: full };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Resolve a friendly alias / partial name to an argv-facing {model, effort?}.
|
|
171
|
+
* Returns null only when the family is unrecognized; the caller then passes
|
|
172
|
+
* the raw input straight to agy. */
|
|
139
173
|
export function resolveModel(
|
|
140
174
|
input: string,
|
|
141
175
|
entries: ModelEntry[],
|
|
142
176
|
defaultThinking: ThinkingTier,
|
|
143
|
-
):
|
|
177
|
+
): ResolvedModel | null {
|
|
144
178
|
const lower = input.toLowerCase().trim();
|
|
145
179
|
|
|
146
180
|
const exact = entries.find((e) => e.full.toLowerCase() === lower);
|
|
147
|
-
if (exact) return exact.full;
|
|
181
|
+
if (exact) return toResolved(exact.full, exact.tier);
|
|
148
182
|
|
|
149
183
|
if (STATIC_SHORT_ALIAS.has(lower)) {
|
|
150
184
|
const target = STATIC_SHORT_ALIAS.get(lower) as string;
|
|
151
185
|
const fromCatalog = entries.find((e) => e.full.toLowerCase() === target.toLowerCase());
|
|
152
|
-
return fromCatalog
|
|
186
|
+
return toResolved(fromCatalog?.full ?? target, fromCatalog?.tier ?? null);
|
|
153
187
|
}
|
|
154
188
|
|
|
155
189
|
let family: Family | null = lower.includes("flash")
|
|
@@ -193,13 +227,14 @@ export function resolveModel(
|
|
|
193
227
|
const familyTiers = new Set(
|
|
194
228
|
candidates.map((e) => e.tier).filter((t): t is ThinkingTier => t !== null),
|
|
195
229
|
);
|
|
196
|
-
if (familyTiers.size === 0) return candidates[0].full;
|
|
230
|
+
if (familyTiers.size === 0) return toResolved(candidates[0].full, null);
|
|
197
231
|
|
|
198
232
|
const preferred =
|
|
199
233
|
tier ??
|
|
200
234
|
(familyTiers.has(defaultThinking) ? defaultThinking : FAMILY_DEFAULT_TIER[family]);
|
|
201
235
|
const chosenTier = nearestTier([...familyTiers], preferred);
|
|
202
|
-
|
|
236
|
+
const picked = candidates.find((e) => e.tier === chosenTier) ?? candidates[0];
|
|
237
|
+
return toResolved(picked.full, picked.tier);
|
|
203
238
|
}
|
|
204
239
|
|
|
205
240
|
/** Parse raw `agy models` text into tool-catalog entries (all families, plus
|
|
@@ -274,7 +309,78 @@ export async function registerAskAntigravityTool(
|
|
|
274
309
|
timeoutMinutes: Type.Optional(
|
|
275
310
|
Type.Number({ description: `Hard cap on the agy run in minutes. Default ${DEFAULT_TIMEOUT_MIN}.` }),
|
|
276
311
|
),
|
|
312
|
+
includeContext: Type.Optional(
|
|
313
|
+
Type.Boolean({
|
|
314
|
+
description:
|
|
315
|
+
"When true, export the current pi conversation (resolved, as markdown) to a temp file inside the workspace and tell agy to read it first. Default false (isolated one-shot). Opt in only when the user explicitly wants agy to see the full conversation; it costs agy tokens to read.",
|
|
316
|
+
}),
|
|
317
|
+
),
|
|
277
318
|
}),
|
|
319
|
+
renderCall(args, theme, _context) {
|
|
320
|
+
// Show RESOLVED model/thinking/mode (config defaults applied) so the
|
|
321
|
+
// row identifies what will actually run, not just explicit args.
|
|
322
|
+
const cfg = loadConfig();
|
|
323
|
+
const requestedModel = (args.model as string | undefined)?.trim() || cfg.defaultModel;
|
|
324
|
+
const resolved =
|
|
325
|
+
resolveModel(requestedModel, entries, cfg.defaultThinking) ?? { model: requestedModel };
|
|
326
|
+
const thinking: ThinkingTier = resolved.effort ?? cfg.defaultThinking;
|
|
327
|
+
const mode: AgyMode = (args.mode as AgyMode | undefined) ?? "accept-edits";
|
|
328
|
+
const useDigest = typeof args.digest === "boolean" ? args.digest : mode === "plan";
|
|
329
|
+
const isContinue =
|
|
330
|
+
typeof args.conversationId === "string" && CONV_ID_RE.test(args.conversationId);
|
|
331
|
+
|
|
332
|
+
const tags: string[] = [`model=${resolved.model}`, `thinking=${thinking}`];
|
|
333
|
+
if (mode !== "accept-edits") tags.push(`mode=${mode}`);
|
|
334
|
+
if (useDigest) tags.push("digest");
|
|
335
|
+
if (isContinue) tags.push("continue");
|
|
336
|
+
if (args.includeContext) tags.push("context=full");
|
|
337
|
+
|
|
338
|
+
let text = theme.fg("mdLink", theme.bold("AskAntigravity "));
|
|
339
|
+
text += `${theme.fg("accent", `[${tags.join(", ")}]`)} `;
|
|
340
|
+
|
|
341
|
+
const prompt = String(args.prompt ?? "");
|
|
342
|
+
const truncated = prompt.length > PREVIEW_MAX_CHARS ? prompt.slice(0, PREVIEW_MAX_CHARS) : prompt;
|
|
343
|
+
const lines = truncated.split("\n").slice(0, PREVIEW_MAX_LINES);
|
|
344
|
+
text += theme.fg("muted", `"${lines.join("\n")}"`);
|
|
345
|
+
if (prompt.length > PREVIEW_MAX_CHARS || prompt.split("\n").length > PREVIEW_MAX_LINES) {
|
|
346
|
+
text += theme.fg("dim", " …");
|
|
347
|
+
}
|
|
348
|
+
return new Text(text, 0, 0);
|
|
349
|
+
},
|
|
350
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
351
|
+
const d = result.details as AgyDetails | undefined;
|
|
352
|
+
if (isPartial) {
|
|
353
|
+
const status = result.content[0]?.type === "text" ? result.content[0].text : "working...";
|
|
354
|
+
return new Text(theme.fg("mdLink", "◉ AskAntigravity ") + theme.fg("muted", status), 0, 0);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const body = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
358
|
+
const errored = d?.exitCode !== 0 || !!d?.aborted || !!d?.timedOut;
|
|
359
|
+
|
|
360
|
+
let text = errored
|
|
361
|
+
? theme.fg("error", "✗ AskAntigravity error")
|
|
362
|
+
: theme.fg("mdLink", "✓ AskAntigravity");
|
|
363
|
+
|
|
364
|
+
const rTags: string[] = [];
|
|
365
|
+
if (d?.resolvedModel || d?.model) rTags.push(`model=${d?.resolvedModel ?? d?.model}`);
|
|
366
|
+
if (d?.thinking) rTags.push(`thinking=${d.thinking}`);
|
|
367
|
+
if (d?.mode && d.mode !== "accept-edits") rTags.push(`mode=${d.mode}`);
|
|
368
|
+
if (d?.includeContext) rTags.push("context=full");
|
|
369
|
+
if (rTags.length) text += ` ${theme.fg("accent", `[${rTags.join(", ")}]`)}`;
|
|
370
|
+
if (d?.durationMs) text += ` ${theme.fg("dim", `${(d.durationMs / 1000).toFixed(1)}s`)}`;
|
|
371
|
+
|
|
372
|
+
if (expanded) {
|
|
373
|
+
if (body) text += `\n${theme.fg("toolOutput", body)}`;
|
|
374
|
+
} else {
|
|
375
|
+
const truncated = body.length > PREVIEW_MAX_CHARS ? body.slice(0, PREVIEW_MAX_CHARS) : body;
|
|
376
|
+
const lines = truncated.split("\n").slice(0, PREVIEW_MAX_LINES);
|
|
377
|
+
if (lines.length) text += `\n${theme.fg("toolOutput", lines.join("\n"))}`;
|
|
378
|
+
if (body.length > PREVIEW_MAX_CHARS || body.split("\n").length > PREVIEW_MAX_LINES) {
|
|
379
|
+
text += `\n${theme.fg("dim", `… (${keyHint("app.tools.expand", "to expand")})`)}`;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return new Text(text, 0, 0);
|
|
383
|
+
},
|
|
278
384
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
279
385
|
// Circular-delegation guard: refuse if already running through the
|
|
280
386
|
// antigravity provider.
|
|
@@ -304,7 +410,9 @@ export async function registerAskAntigravityTool(
|
|
|
304
410
|
};
|
|
305
411
|
}
|
|
306
412
|
const resolved =
|
|
307
|
-
resolveModel(requestedModel, entries, config.defaultThinking) ??
|
|
413
|
+
resolveModel(requestedModel, entries, config.defaultThinking) ?? {
|
|
414
|
+
model: requestedModel,
|
|
415
|
+
};
|
|
308
416
|
|
|
309
417
|
const start = Date.now();
|
|
310
418
|
const cwd = params.cwd || ctx.cwd || process.cwd();
|
|
@@ -313,13 +421,13 @@ export async function registerAskAntigravityTool(
|
|
|
313
421
|
if (!stat.isDirectory()) {
|
|
314
422
|
return {
|
|
315
423
|
content: [{ type: "text", text: `cwd is not a directory: ${cwd}` }],
|
|
316
|
-
details: emptyDetails(requestedModel, resolved),
|
|
424
|
+
details: emptyDetails(requestedModel, resolved.model),
|
|
317
425
|
};
|
|
318
426
|
}
|
|
319
427
|
} catch {
|
|
320
428
|
return {
|
|
321
429
|
content: [{ type: "text", text: `cwd does not exist: ${cwd}` }],
|
|
322
|
-
details: emptyDetails(requestedModel, resolved),
|
|
430
|
+
details: emptyDetails(requestedModel, resolved.model),
|
|
323
431
|
};
|
|
324
432
|
}
|
|
325
433
|
|
|
@@ -337,10 +445,34 @@ export async function registerAskAntigravityTool(
|
|
|
337
445
|
? `(Use compact digests, not full file contents.)\n${params.prompt}`
|
|
338
446
|
: params.prompt;
|
|
339
447
|
|
|
448
|
+
// Opt-in full-context export (isolated stays the default).
|
|
449
|
+
let contextFile: string | null = null;
|
|
450
|
+
if (params.includeContext) {
|
|
451
|
+
try {
|
|
452
|
+
const { messages } = buildSessionContext(ctx.sessionManager.getBranch());
|
|
453
|
+
if (messages.length) {
|
|
454
|
+
const md = renderAgentMessagesMarkdown(messages);
|
|
455
|
+
const ctxDir = askContextDir();
|
|
456
|
+
fs.mkdirSync(ctxDir, { recursive: true });
|
|
457
|
+
contextFile = path.join(
|
|
458
|
+
ctxDir,
|
|
459
|
+
`.ask-context-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.md`,
|
|
460
|
+
);
|
|
461
|
+
fs.writeFileSync(contextFile, md, { mode: 0o600 });
|
|
462
|
+
}
|
|
463
|
+
} catch {
|
|
464
|
+
contextFile = null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const effectivePrompt = contextFile
|
|
468
|
+
? `The full pi conversation context (as markdown) is at: ${contextFile}\nRead that file first for context, then do the task below.\n\n---\n\n${finalPrompt}`
|
|
469
|
+
: finalPrompt;
|
|
470
|
+
|
|
340
471
|
const args: string[] = ["--add-dir", cwd];
|
|
341
472
|
const extra = extraArgs();
|
|
342
473
|
if (extra.length) args.push(...extra);
|
|
343
|
-
if (resolved) args.push("--model", resolved);
|
|
474
|
+
if (resolved.model) args.push("--model", resolved.model);
|
|
475
|
+
if (resolved.effort) args.push("--effort", resolved.effort);
|
|
344
476
|
args.push("--mode", mode);
|
|
345
477
|
// Honor the shared permissions setting (same knob as the provider). Non-
|
|
346
478
|
// interactive -p can't answer a permission prompt, so when this is off
|
|
@@ -348,14 +480,17 @@ export async function registerAskAntigravityTool(
|
|
|
348
480
|
if (config.skipPermissions !== false) args.push("--dangerously-skip-permissions");
|
|
349
481
|
if (isContinuation) args.push("--conversation", rawConvId as string);
|
|
350
482
|
args.push("--print-timeout", `${timeoutMin}m`);
|
|
351
|
-
args.push("-
|
|
483
|
+
if (contextFile) args.push("--add-dir", askContextDir());
|
|
484
|
+
args.push("-p", effectivePrompt);
|
|
352
485
|
|
|
353
486
|
const details: AgyDetails = {
|
|
354
487
|
model: requestedModel,
|
|
355
|
-
resolvedModel: resolved,
|
|
488
|
+
resolvedModel: resolved.model,
|
|
489
|
+
thinking: resolved.effort ?? config.defaultThinking,
|
|
356
490
|
mode,
|
|
357
491
|
digest: useDigest,
|
|
358
492
|
conversationId: isContinuation ? (rawConvId as string) : null,
|
|
493
|
+
includeContext: contextFile !== null,
|
|
359
494
|
exitCode: 0,
|
|
360
495
|
aborted: false,
|
|
361
496
|
timedOut: false,
|
|
@@ -547,16 +682,91 @@ export async function registerAskAntigravityTool(
|
|
|
547
682
|
const msg = err instanceof Error ? err.message : String(err);
|
|
548
683
|
return { content: [{ type: "text", text: `failed to run agy: ${msg}` }], details };
|
|
549
684
|
}
|
|
685
|
+
finally {
|
|
686
|
+
if (contextFile) {
|
|
687
|
+
try {
|
|
688
|
+
fs.unlinkSync(contextFile);
|
|
689
|
+
} catch {}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
550
692
|
},
|
|
551
693
|
});
|
|
552
694
|
}
|
|
553
695
|
|
|
696
|
+
// --- Full-context export (opt-in includeContext) --------------------------
|
|
697
|
+
// NOTE: duplicated per pi-ask-* / bridge package (each is self-contained).
|
|
698
|
+
// Duck-typed over role/content to tolerate AgentMessage's union + custom types.
|
|
699
|
+
|
|
700
|
+
// Tool-call inputs and tool-result bodies are clamped so the exported
|
|
701
|
+
// transcript stays reviewable; the agent can re-read any source file by path.
|
|
702
|
+
// User/assistant prose is kept in full (that IS the conversation).
|
|
703
|
+
const CONTEXT_BLOCK_MAX_CHARS = 2000;
|
|
704
|
+
|
|
705
|
+
function clampBlock(text: unknown, limit = CONTEXT_BLOCK_MAX_CHARS): string {
|
|
706
|
+
const t = String(text ?? "");
|
|
707
|
+
return t.length > limit ? `${t.slice(0, limit)}\n…[truncated, ${t.length - limit} more chars]` : t;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Render resolved pi AgentMessages to a readable markdown transcript.
|
|
711
|
+
* Pure: no IO. Caller writes the returned string to a temp file. */
|
|
712
|
+
function renderAgentMessagesMarkdown(messages: readonly unknown[]): string {
|
|
713
|
+
const lines: string[] = [
|
|
714
|
+
"# Pi conversation context",
|
|
715
|
+
"",
|
|
716
|
+
`_Exported for full-context delegation. ${messages.length} message(s)._`,
|
|
717
|
+
"",
|
|
718
|
+
];
|
|
719
|
+
for (const raw of messages) {
|
|
720
|
+
const m = raw as { role?: string; content?: unknown };
|
|
721
|
+
const role = m.role ?? "message";
|
|
722
|
+
const content = m.content;
|
|
723
|
+
if (role === "assistant") {
|
|
724
|
+
const blocks = (Array.isArray(content) ? content : []) as ReadonlyArray<{
|
|
725
|
+
type: string;
|
|
726
|
+
text?: string;
|
|
727
|
+
name?: string;
|
|
728
|
+
input?: unknown;
|
|
729
|
+
}>;
|
|
730
|
+
const text = blocks
|
|
731
|
+
.filter((b) => b.type === "text")
|
|
732
|
+
.map((b) => b.text ?? "")
|
|
733
|
+
.join("\n");
|
|
734
|
+
if (text.trim()) lines.push("## Assistant", "", text, "");
|
|
735
|
+
for (const b of blocks) {
|
|
736
|
+
if (b.type === "toolCall" || b.type === "tool_use") {
|
|
737
|
+
const input = clampBlock(
|
|
738
|
+
typeof b.input === "string" ? b.input : JSON.stringify(b.input ?? ""),
|
|
739
|
+
500,
|
|
740
|
+
);
|
|
741
|
+
lines.push(`> tool call: ${b.name ?? "(unknown)"}(${input})`, "");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
} else if (role === "toolResult" || role === "tool_result" || role === "tool") {
|
|
745
|
+
const text = contentText(content as any);
|
|
746
|
+
if (text.trim()) lines.push("## Tool result", "", clampBlock(text), "");
|
|
747
|
+
} else {
|
|
748
|
+
const text = contentText(content as any);
|
|
749
|
+
if (text.trim()) lines.push(`## ${role}`, "", clampBlock(text), "");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return lines.join("\n");
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** Centralized scratch dir for full-context exports, following the
|
|
756
|
+
* ~/.pi/extensions-data/<author>/<extension>/ convention (see pi-token-cost-ledger).
|
|
757
|
+
* Derived from getAgentDir() so rebranded distros resolve correctly. */
|
|
758
|
+
function askContextDir(): string {
|
|
759
|
+
return path.join(path.dirname(getAgentDir()), "extensions-data", "estebanforge", "pi-antigravity-bridge");
|
|
760
|
+
}
|
|
761
|
+
|
|
554
762
|
interface AgyDetails {
|
|
555
763
|
model: string | null;
|
|
556
764
|
resolvedModel: string | null;
|
|
765
|
+
thinking: ThinkingTier | null;
|
|
557
766
|
mode: AgyMode;
|
|
558
767
|
digest: boolean;
|
|
559
768
|
conversationId: string | null;
|
|
769
|
+
includeContext: boolean;
|
|
560
770
|
exitCode: number;
|
|
561
771
|
aborted: boolean;
|
|
562
772
|
timedOut: boolean;
|
|
@@ -564,12 +774,19 @@ interface AgyDetails {
|
|
|
564
774
|
stderr: string;
|
|
565
775
|
}
|
|
566
776
|
|
|
567
|
-
function emptyDetails(
|
|
777
|
+
function emptyDetails(
|
|
778
|
+
model: string | null = null,
|
|
779
|
+
resolvedModel: string | null = null,
|
|
780
|
+
thinking: ThinkingTier | null = null,
|
|
781
|
+
includeContext: boolean = false,
|
|
782
|
+
): AgyDetails {
|
|
568
783
|
return {
|
|
569
784
|
model,
|
|
570
785
|
resolvedModel,
|
|
786
|
+
thinking,
|
|
571
787
|
mode: "accept-edits",
|
|
572
788
|
digest: false,
|
|
789
|
+
includeContext,
|
|
573
790
|
conversationId: null,
|
|
574
791
|
exitCode: 0,
|
|
575
792
|
aborted: false,
|
package/src/models.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Discover models from `agy models` and project them into pi's Model shape so
|
|
2
2
|
// they appear in the /model picker as antigravity/<slug>.
|
|
3
3
|
//
|
|
4
|
-
// agy
|
|
5
|
-
//
|
|
6
|
-
// gemini-3.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// agy prints TWO columns per line: "<slug> <display label>". --model takes
|
|
5
|
+
// ONLY the slug (col 1); the label is display-only. Verified live, e.g.:
|
|
6
|
+
// gemini-3.6-flash-high Gemini 3.6 Flash (High)
|
|
7
|
+
// gemini-3.6-flash-medium Gemini 3.6 Flash (Medium) (+ -low)
|
|
8
|
+
// gemini-3.1-pro-high Gemini 3.1 Pro (High) (Pro has NO medium)
|
|
9
|
+
// claude-sonnet-4-6 Claude Sonnet 4.6 (Thinking) (fixed, no tiers)
|
|
10
|
+
// gpt-oss-120b-medium GPT-OSS 120B (Medium) (fixed, no tiers)
|
|
9
11
|
//
|
|
10
12
|
// Gemini models are collapsed to a BASE slug (gemini-3.6-flash) and exposed
|
|
11
13
|
// with a thinking-effort toggle whose levels match exactly the tiers agy
|
|
@@ -34,11 +36,13 @@ const EFFORT_RANK: Record<AgyEffort, number> = { low: 0, medium: 1, high: 2 };
|
|
|
34
36
|
* IS its suffix here, claude-opus-4-6-thinking is not a tier). */
|
|
35
37
|
const TIER_RE = /^(.+)-(high|medium|low)$/;
|
|
36
38
|
|
|
37
|
-
/** agy emits clean slug ids (gemini-3.6-flash-high).
|
|
38
|
-
* banner / auth / "Fetching models…" line can't register as a
|
|
39
|
-
* leading-dash token (e.g. "-high") can't reach agy's flag
|
|
40
|
-
* --model. First char must be alphanumeric
|
|
41
|
-
|
|
39
|
+
/** agy emits clean slug ids (gemini-3.6-flash-high). Validate col1 of each
|
|
40
|
+
* line so a banner / auth / "Fetching models…" line can't register as a
|
|
41
|
+
* model, and a leading-dash token (e.g. "-high") can't reach agy's flag
|
|
42
|
+
* parser as --model. First char must be alphanumeric; the slug must contain
|
|
43
|
+
* at least one hyphen (every real agy slug does: family-version-name), which
|
|
44
|
+
* also drops a prose banner word split out of col1 ("Available"). */
|
|
45
|
+
const MODEL_LINE_RE = /^[A-Za-z0-9][A-Za-z0-9._]*-[A-Za-z0-9._-]*$/;
|
|
42
46
|
|
|
43
47
|
/** Model families VERIFIED to accept base-slug + --effort. Only these collapse
|
|
44
48
|
* to a base slug with a thinking toggle. Any other family stays as agy's exact
|
|
@@ -210,22 +214,23 @@ export async function loadModelCatalogRaw(
|
|
|
210
214
|
* medium) or none (claude-sonnet-4-6) means fixed thinking, where --effort is
|
|
211
215
|
* unsupported. Insertion order of first-seen bases is preserved. */
|
|
212
216
|
export function entriesFromRaw(raw: string): AgyModelEntry[] {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
.filter((line) => MODEL_LINE_RE.test(line));
|
|
217
|
+
// agy prints TWO columns: "<slug> <display label>". --model takes only the
|
|
218
|
+
// slug, so split col1 and validate THAT; the label is display-only. A
|
|
219
|
+
// bare-slug line (no whitespace) splits to itself, so this also tolerates
|
|
220
|
+
// the legacy one-column shape.
|
|
218
221
|
const groups = new Map<string, { lines: string[]; tiers: Set<AgyEffort> }>();
|
|
219
|
-
for (const line of
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
+
for (const line of raw.split("\n")) {
|
|
223
|
+
const slug = line.trim().split(/\s+/)[0] ?? "";
|
|
224
|
+
if (!slug || !MODEL_LINE_RE.test(slug)) continue;
|
|
225
|
+
const m = TIER_RE.exec(slug);
|
|
226
|
+
const base = m ? (m[1] as string) : slug;
|
|
222
227
|
const tier = m ? (m[2] as AgyEffort) : null;
|
|
223
228
|
let g = groups.get(base);
|
|
224
229
|
if (!g) {
|
|
225
230
|
g = { lines: [], tiers: new Set<AgyEffort>() };
|
|
226
231
|
groups.set(base, g);
|
|
227
232
|
}
|
|
228
|
-
g.lines.push(
|
|
233
|
+
g.lines.push(slug);
|
|
229
234
|
if (tier) g.tiers.add(tier);
|
|
230
235
|
}
|
|
231
236
|
const entries: AgyModelEntry[] = [];
|