@estebanforge/pi-antigravity-bridge 1.0.0 → 1.1.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.
- package/CHANGELOG.md +41 -0
- package/extensions/index.ts +1 -0
- package/package.json +4 -3
- package/src/ask-tool.ts +11 -10
- package/src/mcp-server.ts +1 -1
- package/src/models.ts +102 -52
- package/src/provider.ts +52 -4
- package/src/runner.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.1.0] - 2026-07-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Full agy catalog in the picker, grouped like agy's own.** Gemini models
|
|
10
|
+
collapse to base entries (gemini-3.6-flash, gemini-3.1-pro) with a
|
|
11
|
+
thinking-effort toggle; Claude (Sonnet/Opus) and GPT-OSS appear as fixed
|
|
12
|
+
entries with no toggle, since their thinking cannot be changed. The earlier
|
|
13
|
+
Gemini-only filter is gone. Google's Antigravity subscription bills all of
|
|
14
|
+
these through agy, so routing Claude here uses the agy quota you already pay
|
|
15
|
+
for (if you also run pi-claude-bridge you will simply see two Claude
|
|
16
|
+
entries).
|
|
17
|
+
- **Reasoning-effort bridging (`agy --effort`).** For an effort-driven Gemini
|
|
18
|
+
base, pi's thinking-effort toggle drives agy's `--effort` (agy 1.1.5+), and
|
|
19
|
+
the toggle only offers the tiers that base actually accepts (Flash:
|
|
20
|
+
low/medium/high; Pro: low/high), so it can never request a tier agy rejects.
|
|
21
|
+
The level is clamped to the base's supported tiers and always passed (a base
|
|
22
|
+
slug is invalid on its own). Fixed models never receive `--effort` (agy
|
|
23
|
+
rejects it for them). Behavior verified by local experiments against agy
|
|
24
|
+
1.1.9.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Breaking: Gemini model ids changed shape.** Effort is no longer part of the
|
|
29
|
+
model id (`antigravity/gemini-3-6-flash-medium` →
|
|
30
|
+
`antigravity/gemini-3-6-flash`); it is now chosen via pi's thinking-effort
|
|
31
|
+
toggle. A persisted default model, a `--model antigravity/...` flag, or a
|
|
32
|
+
scoped-model pattern set before upgrading will need re-selection.
|
|
33
|
+
Claude/GPT-OSS ids keep their qualified shape (e.g.
|
|
34
|
+
`antigravity/claude-sonnet-4-6`).
|
|
35
|
+
- **Requires agy >= 1.1.5.** Base slugs and the `--effort` flag landed there;
|
|
36
|
+
older agy rejects every Gemini entry with a flag error.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- **AskAntigravity model resolution** now parses agy's stable-slug catalog
|
|
41
|
+
(`gemini-3.6-flash-high`, `claude-sonnet-4-6`) instead of the legacy human
|
|
42
|
+
names. The `sonnet`/`opus`/`gpt-oss` aliases and the `/agy thinking` tier had
|
|
43
|
+
silently resolved to invalid `--model` values since agy switched to slugs;
|
|
44
|
+
they now resolve to exact valid slugs.
|
|
45
|
+
|
|
5
46
|
## [1.0.0] - 2026-07-29
|
|
6
47
|
|
|
7
48
|
First release. A streaming Gemini model provider for pi, built on Google's
|
package/extensions/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"type": "module",
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsc --noEmit",
|
|
31
|
-
"test": "
|
|
31
|
+
"test": "vitest run",
|
|
32
32
|
"prepublishOnly": "npm run build && npm test",
|
|
33
33
|
"run-agy": "tsx scripts/run-agy.ts",
|
|
34
34
|
"decode-db": "tsx scripts/decode-db.ts",
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"@types/node": "^22.0.0",
|
|
62
62
|
"tsx": "^4.19.0",
|
|
63
63
|
"typebox": "^1.1.38",
|
|
64
|
-
"typescript": "^5.8.0"
|
|
64
|
+
"typescript": "^5.8.0",
|
|
65
|
+
"vitest": "^4.1.7"
|
|
65
66
|
},
|
|
66
67
|
"dependencies": {
|
|
67
68
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
package/src/ask-tool.ts
CHANGED
|
@@ -43,16 +43,17 @@ const TIER_RANK: Record<ThinkingTier, number> = { low: 0, medium: 1, high: 2 };
|
|
|
43
43
|
|
|
44
44
|
// Static alias overlay for non-Gemini models agy may or may not surface.
|
|
45
45
|
// Live catalog entries win on case-insensitive full-string equality; the
|
|
46
|
-
// overlay resolves the alias when agy doesn't list it.
|
|
46
|
+
// overlay resolves the alias when agy doesn't list it. Names are agy's stable
|
|
47
|
+
// slugs (the same ids `agy models` prints and `--model` accepts).
|
|
47
48
|
const STATIC_ALIAS_OVERLAY: ReadonlyArray<ModelEntry> = [
|
|
48
|
-
{ full: "
|
|
49
|
-
{ full: "
|
|
50
|
-
{ full: "
|
|
49
|
+
{ full: "claude-sonnet-4-6", family: "other", version: null, tier: null },
|
|
50
|
+
{ full: "claude-opus-4-6-thinking", family: "other", version: null, tier: null },
|
|
51
|
+
{ full: "gpt-oss-120b-medium", family: "other", version: null, tier: null },
|
|
51
52
|
];
|
|
52
53
|
const STATIC_SHORT_ALIAS: ReadonlyMap<string, string> = new Map([
|
|
53
|
-
["sonnet", "
|
|
54
|
-
["opus", "
|
|
55
|
-
["gpt-oss", "
|
|
54
|
+
["sonnet", "claude-sonnet-4-6"],
|
|
55
|
+
["opus", "claude-opus-4-6-thinking"],
|
|
56
|
+
["gpt-oss", "gpt-oss-120b-medium"],
|
|
56
57
|
]);
|
|
57
58
|
|
|
58
59
|
// agy conversation ids are UUID DB-stems. First char must be alphanumeric so a
|
|
@@ -77,7 +78,7 @@ COMPACT OUTPUT (param: digest): when true, the prompt is prefixed to request com
|
|
|
77
78
|
type Family = "flash" | "pro" | "other";
|
|
78
79
|
|
|
79
80
|
interface ModelEntry {
|
|
80
|
-
full: string; // exact agy
|
|
81
|
+
full: string; // exact agy slug, e.g. "gemini-3.6-flash-medium"
|
|
81
82
|
family: Family;
|
|
82
83
|
version: string | null; // "3.6"
|
|
83
84
|
tier: ThinkingTier | null;
|
|
@@ -119,7 +120,7 @@ function parseModelLine(line: string): ModelEntry | null {
|
|
|
119
120
|
: "other";
|
|
120
121
|
const versionMatch = lower.match(/(\d+\.\d+)/);
|
|
121
122
|
const version = versionMatch ? versionMatch[1] : null;
|
|
122
|
-
const tierMatch = lower.match(
|
|
123
|
+
const tierMatch = lower.match(/-(low|medium|high)$/);
|
|
123
124
|
const tier = tierMatch ? (tierMatch[1] as ThinkingTier) : null;
|
|
124
125
|
return { full, family, version, tier };
|
|
125
126
|
}
|
|
@@ -248,7 +249,7 @@ export async function registerAskAntigravityTool(
|
|
|
248
249
|
model: Type.Optional(
|
|
249
250
|
Type.String({
|
|
250
251
|
description:
|
|
251
|
-
"Model alias or exact id. Friendly: 'flash', 'pro', 'gemini'. Add a tier: 'flash high'. Pin a version: '3.5 flash'. Exact: '
|
|
252
|
+
"Model alias or exact id. Friendly: 'flash', 'pro', 'gemini'. Add a tier: 'flash high'. Pin a version: '3.5 flash'. Exact: 'gemini-3.6-flash-medium'. Omit for the configured default.",
|
|
252
253
|
}),
|
|
253
254
|
),
|
|
254
255
|
mode: Type.Optional(
|
package/src/mcp-server.ts
CHANGED
|
@@ -292,7 +292,7 @@ export async function startMcpServer(
|
|
|
292
292
|
|
|
293
293
|
const makeServer = (signal: AbortSignal) => {
|
|
294
294
|
const s = new Server(
|
|
295
|
-
{ name: "pi-antigravity-bridge", version: "1.
|
|
295
|
+
{ name: "pi-antigravity-bridge", version: "1.1.0" },
|
|
296
296
|
{ capabilities: { tools: {} } },
|
|
297
297
|
);
|
|
298
298
|
s.setRequestHandler(ListToolsRequestSchema, listHandler);
|
package/src/models.ts
CHANGED
|
@@ -1,29 +1,62 @@
|
|
|
1
|
-
// Discover
|
|
2
|
-
//
|
|
1
|
+
// Discover models from `agy models` and project them into pi's Model shape so
|
|
2
|
+
// they appear in the /model picker as antigravity/<slug>.
|
|
3
3
|
//
|
|
4
|
-
// agy
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// agy lists effort-qualified slugs one per line, e.g.:
|
|
5
|
+
// gemini-3.6-flash-high / -medium / -low
|
|
6
|
+
// gemini-3.1-pro-high / -low (Pro has NO medium variant)
|
|
7
|
+
// claude-sonnet-4-6 (fixed thinking, no effort tiers)
|
|
8
|
+
// gpt-oss-120b-medium (fixed, no effort tiers)
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// Gemini models are collapsed to a BASE slug (gemini-3.6-flash) and exposed
|
|
11
|
+
// with a thinking-effort toggle whose levels match exactly the tiers agy
|
|
12
|
+
// offers that base (verified: Pro rejects medium). The picked level is sent as
|
|
13
|
+
// agy --effort; a base slug is INVALID on its own, so effort is always passed.
|
|
14
|
+
// Claude and GPT-OSS keep agy's exact slug with no toggle: their thinking is
|
|
15
|
+
// fixed and agy rejects --effort for them. Google's Antigravity subscription
|
|
16
|
+
// bills all of these through agy.
|
|
13
17
|
|
|
14
18
|
import { spawn } from "node:child_process";
|
|
15
19
|
import fs from "node:fs";
|
|
16
20
|
import os from "node:os";
|
|
17
21
|
import path from "node:path";
|
|
18
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
22
|
+
import type { Api, Model, ThinkingLevelMap } from "@earendil-works/pi-ai";
|
|
19
23
|
|
|
20
24
|
const DISCOVERY_TIMEOUT_MS = 8_000;
|
|
21
25
|
|
|
26
|
+
/** Reasoning-effort tiers agy accepts via --effort. */
|
|
27
|
+
export type AgyEffort = "low" | "medium" | "high";
|
|
28
|
+
|
|
29
|
+
/** low < medium < high, for sorting/clamping. */
|
|
30
|
+
const EFFORT_RANK: Record<AgyEffort, number> = { low: 0, medium: 1, high: 2 };
|
|
31
|
+
|
|
32
|
+
/** Split an agy slug into (base, tier). tier is null when the slug has no
|
|
33
|
+
* -high/-medium/-low suffix (claude-sonnet-4-6, gpt-oss-120b-medium's "medium"
|
|
34
|
+
* IS its suffix here, claude-opus-4-6-thinking is not a tier). */
|
|
35
|
+
const TIER_RE = /^(.+)-(high|medium|low)$/;
|
|
36
|
+
|
|
37
|
+
/** agy emits clean slug ids (gemini-3.6-flash-high). Reject anything else so a
|
|
38
|
+
* banner / auth / "Fetching models…" line can't register as a model, and a
|
|
39
|
+
* leading-dash token (e.g. "-high") can't reach agy's flag parser as
|
|
40
|
+
* --model. First char must be alphanumeric (no leading dash, no dot). */
|
|
41
|
+
const MODEL_LINE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
42
|
+
|
|
43
|
+
/** Model families VERIFIED to accept base-slug + --effort. Only these collapse
|
|
44
|
+
* to a base slug with a thinking toggle. Any other family stays as agy's exact
|
|
45
|
+
* qualified slug (always valid on its own), so an unknown or fixed-thinking
|
|
46
|
+
* family degrades safely instead of forcing an unsupported --effort. Add a
|
|
47
|
+
* family here only after confirming base+--effort is accepted for it. */
|
|
48
|
+
const EFFORT_CAPABLE_FAMILIES: readonly RegExp[] = [/^gemini-/];
|
|
49
|
+
|
|
22
50
|
export interface AgyModelEntry {
|
|
23
|
-
/** Exact agy string
|
|
51
|
+
/** Exact agy --model string to pass: a base slug ("gemini-3.6-flash") when
|
|
52
|
+
* effort-driven, else agy's full qualified slug ("claude-opus-4-6-thinking"). */
|
|
24
53
|
full: string;
|
|
25
|
-
/** pi model id, e.g. "gemini-3-6-flash
|
|
54
|
+
/** pi model id, e.g. "gemini-3-6-flash". */
|
|
26
55
|
id: string;
|
|
56
|
+
/** Present iff this is an effort-driven base slug. Lists the tiers agy
|
|
57
|
+
* accepts for it; pi's thinking toggle picks among them and we always pass
|
|
58
|
+
* --effort. Absent => fixed model, never pass --effort. */
|
|
59
|
+
efforts?: AgyEffort[];
|
|
27
60
|
}
|
|
28
61
|
|
|
29
62
|
/** Spawn `agy models` and return its raw stdout text. Returns "" on any
|
|
@@ -169,33 +202,50 @@ export async function loadModelCatalogRaw(
|
|
|
169
202
|
return raw;
|
|
170
203
|
}
|
|
171
204
|
|
|
172
|
-
/** Parse raw `agy models` text into the provider's
|
|
205
|
+
/** Parse raw `agy models` text into the provider's model entries.
|
|
206
|
+
*
|
|
207
|
+
* Effort-driven Gemini bases (>= 2 tier variants in the catalog) collapse to
|
|
208
|
+
* one BASE-slug entry carrying the tiers they accept. Models with 0 or 1 tier
|
|
209
|
+
* variants keep agy's exact qualified slug: a single suffix (gpt-oss-120b-
|
|
210
|
+
* medium) or none (claude-sonnet-4-6) means fixed thinking, where --effort is
|
|
211
|
+
* unsupported. Insertion order of first-seen bases is preserved. */
|
|
173
212
|
export function entriesFromRaw(raw: string): AgyModelEntry[] {
|
|
174
|
-
|
|
213
|
+
const lines = raw
|
|
175
214
|
.split("\n")
|
|
176
215
|
.map((line) => line.trim())
|
|
177
216
|
.filter((line) => line.length > 0)
|
|
178
|
-
.filter(
|
|
179
|
-
|
|
180
|
-
|
|
217
|
+
.filter((line) => MODEL_LINE_RE.test(line));
|
|
218
|
+
const groups = new Map<string, { lines: string[]; tiers: Set<AgyEffort> }>();
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
const m = TIER_RE.exec(line);
|
|
221
|
+
const base = m ? (m[1] as string) : line;
|
|
222
|
+
const tier = m ? (m[2] as AgyEffort) : null;
|
|
223
|
+
let g = groups.get(base);
|
|
224
|
+
if (!g) {
|
|
225
|
+
g = { lines: [], tiers: new Set<AgyEffort>() };
|
|
226
|
+
groups.set(base, g);
|
|
227
|
+
}
|
|
228
|
+
g.lines.push(line);
|
|
229
|
+
if (tier) g.tiers.add(tier);
|
|
230
|
+
}
|
|
231
|
+
const entries: AgyModelEntry[] = [];
|
|
232
|
+
for (const [base, g] of groups) {
|
|
233
|
+
const efforts = [...g.tiers].sort((a, b) => EFFORT_RANK[a] - EFFORT_RANK[b]);
|
|
234
|
+
if (efforts.length >= 2 && EFFORT_CAPABLE_FAMILIES.some((re) => re.test(base))) {
|
|
235
|
+
entries.push({ full: base, id: slugify(base), efforts });
|
|
236
|
+
} else {
|
|
237
|
+
for (const line of g.lines) entries.push({ full: line, id: slugify(line) });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return entries.filter((e) => e.id.length > 0);
|
|
181
241
|
}
|
|
182
242
|
|
|
183
|
-
/** Run `agy models`, return parsed
|
|
243
|
+
/** Run `agy models`, return parsed model entries. Returns [] on any failure
|
|
184
244
|
* (non-fatal - the provider falls back to a hardcoded set). */
|
|
185
245
|
export async function discoverAgyModels(binary: string): Promise<AgyModelEntry[]> {
|
|
186
246
|
return entriesFromRaw(await spawnAgyModelsRaw(binary));
|
|
187
247
|
}
|
|
188
248
|
|
|
189
|
-
/** Gemini models only. Case-insensitive: the name must contain "gemini" and
|
|
190
|
-
* NOT be a Claude/GPT-OSS entry (defensive - agy could rename lines). */
|
|
191
|
-
function isGeminiModel(line: string): boolean {
|
|
192
|
-
const l = line.toLowerCase();
|
|
193
|
-
if (!l.includes("gemini")) return false;
|
|
194
|
-
if (l.includes("claude")) return false;
|
|
195
|
-
if (l.includes("gpt")) return false;
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
249
|
/** "Gemini 3.6 Flash (Medium)" -> "gemini-3-6-flash-medium".
|
|
200
250
|
* Lowercase, non-alphanumerics -> "-", collapsed, trimmed. */
|
|
201
251
|
export function slugify(full: string): string {
|
|
@@ -205,17 +255,22 @@ export function slugify(full: string): string {
|
|
|
205
255
|
.replace(/^-+|-+$/g, "");
|
|
206
256
|
}
|
|
207
257
|
|
|
258
|
+
/** Build the thinkingLevelMap for an effort-driven base: hide "off" and
|
|
259
|
+
* "minimal" always (agy has no no-thinking mode; a base REQUIRES an effort),
|
|
260
|
+
* and hide any of low/medium/high the base doesn't offer (e.g. Pro has no
|
|
261
|
+
* medium). pi's getSupportedThinkingLevels treats a null value as hidden, so
|
|
262
|
+
* the toggle then shows exactly agy's slider stops. */
|
|
263
|
+
function thinkingLevelMapFor(efforts: readonly AgyEffort[]): ThinkingLevelMap {
|
|
264
|
+
const map: Record<string, string | null> = { off: null, minimal: null };
|
|
265
|
+
for (const level of ["low", "medium", "high"] as const) {
|
|
266
|
+
if (!efforts.includes(level)) map[level] = null;
|
|
267
|
+
}
|
|
268
|
+
return map as ThinkingLevelMap;
|
|
269
|
+
}
|
|
270
|
+
|
|
208
271
|
/** Project an agy entry to pi's Model shape. */
|
|
209
272
|
export function toPiModel(entry: AgyModelEntry): Model<Api> {
|
|
210
|
-
const
|
|
211
|
-
? "high"
|
|
212
|
-
: /\(low\)/i.test(entry.full)
|
|
213
|
-
? "low"
|
|
214
|
-
: "medium";
|
|
215
|
-
// "reasoning" gates pi's thinking-effort UI. Gemini reasons at every tier,
|
|
216
|
-
// but we only expose the toggle for High to avoid implying control we
|
|
217
|
-
// don't actually bridge to agy.
|
|
218
|
-
const reasoning = tier === "high";
|
|
273
|
+
const effortDriven = !!entry.efforts && entry.efforts.length > 0;
|
|
219
274
|
return {
|
|
220
275
|
id: entry.id,
|
|
221
276
|
name: entry.full,
|
|
@@ -225,7 +280,11 @@ export function toPiModel(entry: AgyModelEntry): Model<Api> {
|
|
|
225
280
|
// pi requires non-empty values. The "agy-bridge" api string is a custom
|
|
226
281
|
// sentinel that no built-in provider claims, so it can never collide.
|
|
227
282
|
baseUrl: "agy-bridge://antigravity",
|
|
228
|
-
reasoning
|
|
283
|
+
// reasoning=true only for effort-driven bases => pi shows the toggle.
|
|
284
|
+
// Fixed models (Claude/GPT-OSS) get no toggle: their thinking can't be
|
|
285
|
+
// changed and agy rejects --effort for them.
|
|
286
|
+
reasoning: effortDriven,
|
|
287
|
+
...(effortDriven ? { thinkingLevelMap: thinkingLevelMapFor(entry.efforts!) } : {}),
|
|
229
288
|
// agy's -p prompt is text-only. Advertising image input would let pi
|
|
230
289
|
// offer image attach, but extractUserPrompt silently drops image blocks,
|
|
231
290
|
// so the user would be misled. Keep input text-only until agy supports
|
|
@@ -244,18 +303,9 @@ export function toPiModel(entry: AgyModelEntry): Model<Api> {
|
|
|
244
303
|
* can still select a model and get a clear runtime error instead of an empty
|
|
245
304
|
* list. Update these when agy ships new Gemini versions. */
|
|
246
305
|
export const FALLBACK_MODELS: AgyModelEntry[] = [
|
|
247
|
-
{ full: "
|
|
248
|
-
{ full: "
|
|
249
|
-
{ full: "
|
|
306
|
+
{ full: "gemini-3.6-flash", id: "gemini-3-6-flash", efforts: ["low", "medium", "high"] },
|
|
307
|
+
{ full: "gemini-3.1-pro", id: "gemini-3-1-pro", efforts: ["low", "high"] },
|
|
308
|
+
{ full: "claude-sonnet-4-6", id: "claude-sonnet-4-6" },
|
|
250
309
|
];
|
|
251
310
|
|
|
252
|
-
|
|
253
|
-
* - the provider calls this once per turn. Returns null on miss (caller
|
|
254
|
-
* falls back to passthrough, agy will likely reject). */
|
|
255
|
-
export function resolveAgyString(
|
|
256
|
-
piModelId: string,
|
|
257
|
-
entries: AgyModelEntry[],
|
|
258
|
-
): string | null {
|
|
259
|
-
const found = entries.find((e) => e.id === piModelId);
|
|
260
|
-
return found ? found.full : null;
|
|
261
|
-
}
|
|
311
|
+
|
package/src/provider.ts
CHANGED
|
@@ -25,11 +25,12 @@ import {
|
|
|
25
25
|
type Message,
|
|
26
26
|
type Model,
|
|
27
27
|
type SimpleStreamOptions,
|
|
28
|
+
type ThinkingLevel,
|
|
28
29
|
type Usage,
|
|
29
30
|
} from "@earendil-works/pi-ai";
|
|
30
31
|
import type { Api } from "@earendil-works/pi-ai";
|
|
31
32
|
import { runAgyTurn, type AgyEvent, type AgyRunOptions } from "./runner.js";
|
|
32
|
-
import {
|
|
33
|
+
import { type AgyEffort, type AgyModelEntry } from "./models.js";
|
|
33
34
|
import { SessionStore } from "./sessions.js";
|
|
34
35
|
import { loadConfig } from "./config.js";
|
|
35
36
|
import path from "node:path";
|
|
@@ -246,6 +247,46 @@ export interface StreamSimpleDeps {
|
|
|
246
247
|
runAgyTurn?: typeof runAgyTurn;
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
/** pi thinking-effort order mirrors agy's, for clamping. */
|
|
251
|
+
const AGY_EFFORT_ORDER: readonly AgyEffort[] = ["low", "medium", "high"];
|
|
252
|
+
|
|
253
|
+
/** Map pi's thinking level onto one of the tiers `efforts` the base actually
|
|
254
|
+
* supports, clamping to the nearest available. agy rejects an effort tier a
|
|
255
|
+
* base doesn't list (e.g. medium on Pro), so we never emit one. A base slug is
|
|
256
|
+
* invalid without --effort, so when pi sends no level we default to the
|
|
257
|
+
* middle tier (or the highest available). */
|
|
258
|
+
export function toAgyEffort(
|
|
259
|
+
reasoning: ThinkingLevel | undefined,
|
|
260
|
+
efforts: readonly AgyEffort[],
|
|
261
|
+
): AgyEffort {
|
|
262
|
+
let candidate: AgyEffort;
|
|
263
|
+
switch (reasoning) {
|
|
264
|
+
case "minimal":
|
|
265
|
+
case "low":
|
|
266
|
+
candidate = "low";
|
|
267
|
+
break;
|
|
268
|
+
case "medium":
|
|
269
|
+
candidate = "medium";
|
|
270
|
+
break;
|
|
271
|
+
case "high":
|
|
272
|
+
case "xhigh":
|
|
273
|
+
case "max":
|
|
274
|
+
candidate = "high";
|
|
275
|
+
break;
|
|
276
|
+
default:
|
|
277
|
+
candidate = efforts[0] ?? "low";
|
|
278
|
+
}
|
|
279
|
+
if (efforts.includes(candidate)) return candidate;
|
|
280
|
+
const i = AGY_EFFORT_ORDER.indexOf(candidate);
|
|
281
|
+
for (let j = i; j < AGY_EFFORT_ORDER.length; j++) {
|
|
282
|
+
if (efforts.includes(AGY_EFFORT_ORDER[j])) return AGY_EFFORT_ORDER[j];
|
|
283
|
+
}
|
|
284
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
285
|
+
if (efforts.includes(AGY_EFFORT_ORDER[j])) return AGY_EFFORT_ORDER[j];
|
|
286
|
+
}
|
|
287
|
+
return efforts[0] ?? "low";
|
|
288
|
+
}
|
|
289
|
+
|
|
249
290
|
/** Build the streamSimple closure. Captures the model catalog + session store
|
|
250
291
|
* resolved at extension load. */
|
|
251
292
|
export function createStreamSimple(
|
|
@@ -311,20 +352,27 @@ async function runTurn(
|
|
|
311
352
|
const digest = buildContextDigest(context.messages, watermark);
|
|
312
353
|
const fullPrompt = digest ? `${DIGEST_PREAMBLE}\n\n${digest}\n\n---\n\n${prompt}` : prompt;
|
|
313
354
|
|
|
314
|
-
// Resolve the pi model id to
|
|
315
|
-
//
|
|
355
|
+
// Resolve the pi model id to its catalog entry. On a miss, fall through to
|
|
356
|
+
// the id itself - agy will likely reject, but the error reaches the user
|
|
316
357
|
// instead of a silent no-op.
|
|
317
|
-
const
|
|
358
|
+
const entry = entries.find((e) => e.id === model.id) ?? null;
|
|
359
|
+
const agyModel = entry?.full ?? model.id;
|
|
318
360
|
|
|
319
361
|
// Runtime config (mode, permissions). Loaded fresh each turn so /agy
|
|
320
362
|
// toggles take effect immediately without a reload.
|
|
321
363
|
const config = loadConfig();
|
|
322
364
|
|
|
365
|
+
// Effort-driven bases always need --effort (a base slug is invalid on its
|
|
366
|
+
// own); fixed models never get it (agy rejects --effort for them). For an
|
|
367
|
+
// effort-driven base we clamp pi's level to the tiers agy offers it.
|
|
368
|
+
const effort = entry?.efforts?.length ? toAgyEffort(options?.reasoning, entry.efforts) : undefined;
|
|
369
|
+
|
|
323
370
|
const runOpts: AgyRunOptions = {
|
|
324
371
|
cwd,
|
|
325
372
|
model: agyModel,
|
|
326
373
|
mode: config.mode,
|
|
327
374
|
skipPermissions: config.skipPermissions,
|
|
375
|
+
effort,
|
|
328
376
|
prompt: fullPrompt,
|
|
329
377
|
conversationId: existing?.conversationId ?? null,
|
|
330
378
|
baseStepIdx: existing?.lastStepIdx ?? -1,
|
package/src/runner.ts
CHANGED
|
@@ -88,6 +88,9 @@ export interface AgyRunOptions {
|
|
|
88
88
|
/** Exact agy model string, e.g. "Gemini 3.6 Flash (Medium)". Caller
|
|
89
89
|
* resolves aliases; the runner passes this through verbatim. */
|
|
90
90
|
model?: string;
|
|
91
|
+
/** Reasoning effort passed to agy --effort (low|medium|high). Omit to let
|
|
92
|
+
* the model slug's own effort stand (requires agy >= 1.1.5). */
|
|
93
|
+
effort?: "low" | "medium" | "high";
|
|
91
94
|
/** agy execution mode. accept-edits = agy applies edits; plan = review-only. */
|
|
92
95
|
mode?: "accept-edits" | "plan";
|
|
93
96
|
/** Pass --dangerously-skip-permissions so commands don't hang on an
|
|
@@ -168,6 +171,7 @@ export async function runAgyTurn(
|
|
|
168
171
|
// this dir, so its agy stays plain (no recursion).
|
|
169
172
|
if (bridgeMcpConfigExists()) args.push("--add-dir", bridgeMcpConfigDir());
|
|
170
173
|
if (opts.model) args.push("--model", opts.model);
|
|
174
|
+
if (opts.effort) args.push("--effort", opts.effort);
|
|
171
175
|
args.push("--mode", mode);
|
|
172
176
|
// Without this, any run_command triggers an interactive permission prompt
|
|
173
177
|
// that hangs forever in non-interactive print mode (no TTY to answer y/n).
|