remove gemini cli and antigravity support
This commit is contained in:
@@ -104,8 +104,6 @@ For each built-in provider, pi maintains a list of tool-capable models, updated
|
||||
- Anthropic Claude Pro/Max
|
||||
- OpenAI ChatGPT Plus/Pro (Codex)
|
||||
- GitHub Copilot
|
||||
- Google Gemini CLI
|
||||
- Google Antigravity
|
||||
|
||||
**API keys:**
|
||||
- Anthropic
|
||||
|
||||
@@ -199,7 +199,6 @@ The `api` field determines which streaming implementation is used:
|
||||
| `openai-codex-responses` | OpenAI Codex Responses API |
|
||||
| `mistral-conversations` | Mistral SDK Conversations/Chat streaming |
|
||||
| `google-generative-ai` | Google Generative AI API |
|
||||
| `google-gemini-cli` | Google Cloud Code Assist API |
|
||||
| `google-vertex` | Google Vertex AI API |
|
||||
| `bedrock-converse-stream` | Amazon Bedrock Converse API |
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ Use `/login` in interactive mode, then select a provider:
|
||||
- Claude Pro/Max
|
||||
- ChatGPT Plus/Pro (Codex)
|
||||
- GitHub Copilot
|
||||
- Google Gemini CLI
|
||||
- Google Antigravity
|
||||
|
||||
Use `/logout` to clear credentials. Tokens are stored in `~/.pi/agent/auth.json` and auto-refresh when expired.
|
||||
|
||||
@@ -30,8 +28,6 @@ Use `/logout` to clear credentials. Tokens are stored in `~/.pi/agent/auth.json`
|
||||
|
||||
### Google Providers
|
||||
|
||||
- **Gemini CLI**: Standard Gemini models via Cloud Code Assist
|
||||
- **Antigravity**: Sandbox with Gemini 3, Claude, and GPT-OSS models
|
||||
- Both free with any Google account, subject to rate limits
|
||||
- For paid Cloud Code Assist: set `GOOGLE_CLOUD_PROJECT` env var
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Start pi and run:
|
||||
/login
|
||||
```
|
||||
|
||||
Then select a provider. Built-in subscription logins include Claude Pro/Max, ChatGPT Plus/Pro (Codex), GitHub Copilot, Google Gemini CLI, and Google Antigravity.
|
||||
Then select a provider. Built-in subscription logins include Claude Pro/Max, ChatGPT Plus/Pro (Codex), and GitHub Copilot.
|
||||
|
||||
### Option 2: API key
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
||||
| `built-in-tool-renderer.ts` | Custom compact rendering for built-in tools (read, bash, edit, write) while keeping original behavior |
|
||||
| `minimal-mode.ts` | Override built-in tool rendering for minimal display (only tool calls, no output in collapsed mode) |
|
||||
| `truncated-tool.ts` | Wraps ripgrep with proper output truncation (50KB/2000 lines) |
|
||||
| `antigravity-image-gen.ts` | Generate images via Google Antigravity with optional save-to-disk modes |
|
||||
| `ssh.ts` | Delegate all tools to a remote machine via SSH using pluggable operations |
|
||||
| `subagent/` | Delegate tasks to specialized subagents with isolated context windows |
|
||||
|
||||
|
||||
@@ -1,418 +0,0 @@
|
||||
/**
|
||||
* Antigravity Image Generation
|
||||
*
|
||||
* Generates images via Google Antigravity's image models (gemini-3-pro-image, imagen-3).
|
||||
* Returns images as tool result attachments for inline terminal rendering.
|
||||
* Requires OAuth login via /login for google-antigravity.
|
||||
*
|
||||
* Usage:
|
||||
* "Generate an image of a sunset over mountains"
|
||||
* "Create a 16:9 wallpaper of a cyberpunk city"
|
||||
*
|
||||
* Save modes (tool param, env var, or config file):
|
||||
* save=none - Don't save to disk (default)
|
||||
* save=project - Save to <repo>/.pi/generated-images/
|
||||
* save=global - Save to ~/.pi/agent/generated-images/
|
||||
* save=custom - Save to saveDir param or PI_IMAGE_SAVE_DIR
|
||||
*
|
||||
* Environment variables:
|
||||
* PI_IMAGE_SAVE_MODE - Default save mode (none|project|global|custom)
|
||||
* PI_IMAGE_SAVE_DIR - Directory for custom save mode
|
||||
*
|
||||
* Config files (project overrides global):
|
||||
* ~/.pi/agent/extensions/antigravity-image-gen.json
|
||||
* <repo>/.pi/extensions/antigravity-image-gen.json
|
||||
* Example: { "save": "global" }
|
||||
*/
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { StringEnum } from "@mariozechner/pi-ai";
|
||||
import { type ExtensionAPI, getAgentDir, withFileMutationQueue } from "@mariozechner/pi-coding-agent";
|
||||
import { type Static, Type } from "typebox";
|
||||
|
||||
const PROVIDER = "google-antigravity";
|
||||
|
||||
const ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"] as const;
|
||||
|
||||
type AspectRatio = (typeof ASPECT_RATIOS)[number];
|
||||
|
||||
const DEFAULT_MODEL = "gemini-3-pro-image";
|
||||
const DEFAULT_ASPECT_RATIO: AspectRatio = "1:1";
|
||||
const DEFAULT_SAVE_MODE = "none";
|
||||
|
||||
const SAVE_MODES = ["none", "project", "global", "custom"] as const;
|
||||
type SaveMode = (typeof SAVE_MODES)[number];
|
||||
|
||||
const ANTIGRAVITY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
||||
|
||||
const DEFAULT_ANTIGRAVITY_VERSION = "1.21.9";
|
||||
|
||||
const ANTIGRAVITY_HEADERS = {
|
||||
"User-Agent": `antigravity/${process.env.PI_AI_ANTIGRAVITY_VERSION || DEFAULT_ANTIGRAVITY_VERSION} darwin/arm64`,
|
||||
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
||||
"Client-Metadata": JSON.stringify({
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
}),
|
||||
};
|
||||
|
||||
const IMAGE_SYSTEM_INSTRUCTION =
|
||||
"You are an AI image generator. Generate images based on user descriptions. Focus on creating high-quality, visually appealing images that match the user's request.";
|
||||
|
||||
const TOOL_PARAMS = Type.Object({
|
||||
prompt: Type.String({ description: "Image description." }),
|
||||
model: Type.Optional(
|
||||
Type.String({
|
||||
description: "Image model id (e.g., gemini-3-pro-image, imagen-3). Default: gemini-3-pro-image.",
|
||||
}),
|
||||
),
|
||||
aspectRatio: Type.Optional(StringEnum(ASPECT_RATIOS)),
|
||||
save: Type.Optional(StringEnum(SAVE_MODES)),
|
||||
saveDir: Type.Optional(
|
||||
Type.String({
|
||||
description: "Directory to save image when save=custom. Defaults to PI_IMAGE_SAVE_DIR if set.",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
type ToolParams = Static<typeof TOOL_PARAMS>;
|
||||
|
||||
interface CloudCodeAssistRequest {
|
||||
project: string;
|
||||
model: string;
|
||||
request: {
|
||||
contents: Content[];
|
||||
sessionId?: string;
|
||||
systemInstruction?: { role?: string; parts: { text: string }[] };
|
||||
generationConfig?: {
|
||||
maxOutputTokens?: number;
|
||||
temperature?: number;
|
||||
imageConfig?: { aspectRatio?: string };
|
||||
candidateCount?: number;
|
||||
};
|
||||
safetySettings?: Array<{ category: string; threshold: string }>;
|
||||
};
|
||||
requestType?: string;
|
||||
userAgent?: string;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
interface CloudCodeAssistResponseChunk {
|
||||
response?: {
|
||||
candidates?: Array<{
|
||||
content?: {
|
||||
role: string;
|
||||
parts?: Array<{
|
||||
text?: string;
|
||||
inlineData?: {
|
||||
mimeType?: string;
|
||||
data?: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
}>;
|
||||
usageMetadata?: {
|
||||
promptTokenCount?: number;
|
||||
candidatesTokenCount?: number;
|
||||
thoughtsTokenCount?: number;
|
||||
totalTokenCount?: number;
|
||||
cachedContentTokenCount?: number;
|
||||
};
|
||||
modelVersion?: string;
|
||||
responseId?: string;
|
||||
};
|
||||
traceId?: string;
|
||||
}
|
||||
|
||||
interface Content {
|
||||
role: "user" | "model";
|
||||
parts: Part[];
|
||||
}
|
||||
|
||||
interface Part {
|
||||
text?: string;
|
||||
inlineData?: {
|
||||
mimeType?: string;
|
||||
data?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ParsedCredentials {
|
||||
accessToken: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
interface ExtensionConfig {
|
||||
save?: SaveMode;
|
||||
saveDir?: string;
|
||||
}
|
||||
|
||||
interface SaveConfig {
|
||||
mode: SaveMode;
|
||||
outputDir?: string;
|
||||
}
|
||||
|
||||
function parseOAuthCredentials(raw: string): ParsedCredentials {
|
||||
let parsed: { token?: string; projectId?: string };
|
||||
try {
|
||||
parsed = JSON.parse(raw) as { token?: string; projectId?: string };
|
||||
} catch {
|
||||
throw new Error("Invalid Google OAuth credentials. Run /login to re-authenticate.");
|
||||
}
|
||||
if (!parsed.token || !parsed.projectId) {
|
||||
throw new Error("Missing token or projectId in Google OAuth credentials. Run /login.");
|
||||
}
|
||||
return { accessToken: parsed.token, projectId: parsed.projectId };
|
||||
}
|
||||
|
||||
function readConfigFile(path: string): ExtensionConfig {
|
||||
if (!existsSync(path)) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const content = readFileSync(path, "utf-8");
|
||||
const parsed = JSON.parse(content) as ExtensionConfig;
|
||||
return parsed ?? {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function loadConfig(cwd: string): ExtensionConfig {
|
||||
const globalPath = join(getAgentDir(), "extensions", "antigravity-image-gen.json");
|
||||
const globalConfig = readConfigFile(globalPath);
|
||||
const projectConfig = readConfigFile(join(cwd, ".pi", "extensions", "antigravity-image-gen.json"));
|
||||
return { ...globalConfig, ...projectConfig };
|
||||
}
|
||||
|
||||
function resolveSaveConfig(params: ToolParams, cwd: string): SaveConfig {
|
||||
const config = loadConfig(cwd);
|
||||
const envMode = (process.env.PI_IMAGE_SAVE_MODE || "").toLowerCase();
|
||||
const paramMode = params.save;
|
||||
const mode = (paramMode || envMode || config.save || DEFAULT_SAVE_MODE) as SaveMode;
|
||||
|
||||
if (!SAVE_MODES.includes(mode)) {
|
||||
return { mode: DEFAULT_SAVE_MODE as SaveMode };
|
||||
}
|
||||
|
||||
if (mode === "project") {
|
||||
return { mode, outputDir: join(cwd, ".pi", "generated-images") };
|
||||
}
|
||||
|
||||
if (mode === "global") {
|
||||
const outputDir = join(getAgentDir(), "generated-images");
|
||||
return { mode, outputDir };
|
||||
}
|
||||
|
||||
if (mode === "custom") {
|
||||
const dir = params.saveDir || process.env.PI_IMAGE_SAVE_DIR || config.saveDir;
|
||||
if (!dir || !dir.trim()) {
|
||||
throw new Error("save=custom requires saveDir or PI_IMAGE_SAVE_DIR.");
|
||||
}
|
||||
return { mode, outputDir: dir };
|
||||
}
|
||||
|
||||
return { mode };
|
||||
}
|
||||
|
||||
function imageExtension(mimeType: string): string {
|
||||
const lower = mimeType.toLowerCase();
|
||||
if (lower.includes("jpeg") || lower.includes("jpg")) return "jpg";
|
||||
if (lower.includes("gif")) return "gif";
|
||||
if (lower.includes("webp")) return "webp";
|
||||
return "png";
|
||||
}
|
||||
|
||||
async function saveImage(base64Data: string, mimeType: string, outputDir: string): Promise<string> {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const ext = imageExtension(mimeType);
|
||||
const filename = `image-${timestamp}-${randomUUID().slice(0, 8)}.${ext}`;
|
||||
const filePath = join(outputDir, filename);
|
||||
await withFileMutationQueue(filePath, async () => {
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
await writeFile(filePath, Buffer.from(base64Data, "base64"));
|
||||
});
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function buildRequest(prompt: string, model: string, projectId: string, aspectRatio: string): CloudCodeAssistRequest {
|
||||
return {
|
||||
project: projectId,
|
||||
model,
|
||||
request: {
|
||||
contents: [
|
||||
{
|
||||
role: "user",
|
||||
parts: [{ text: prompt }],
|
||||
},
|
||||
],
|
||||
systemInstruction: {
|
||||
parts: [{ text: IMAGE_SYSTEM_INSTRUCTION }],
|
||||
},
|
||||
generationConfig: {
|
||||
imageConfig: { aspectRatio },
|
||||
candidateCount: 1,
|
||||
},
|
||||
safetySettings: [
|
||||
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_ONLY_HIGH" },
|
||||
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_ONLY_HIGH" },
|
||||
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_ONLY_HIGH" },
|
||||
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_ONLY_HIGH" },
|
||||
{ category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "BLOCK_ONLY_HIGH" },
|
||||
],
|
||||
},
|
||||
requestType: "agent",
|
||||
requestId: `agent-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
||||
userAgent: "antigravity",
|
||||
};
|
||||
}
|
||||
|
||||
async function parseSseForImage(
|
||||
response: Response,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ image: { data: string; mimeType: string }; text: string[] }> {
|
||||
if (!response.body) {
|
||||
throw new Error("No response body");
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
const textParts: string[] = [];
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
if (signal?.aborted) {
|
||||
throw new Error("Request was aborted");
|
||||
}
|
||||
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data:")) continue;
|
||||
const jsonStr = line.slice(5).trim();
|
||||
if (!jsonStr) continue;
|
||||
|
||||
let chunk: CloudCodeAssistResponseChunk;
|
||||
try {
|
||||
chunk = JSON.parse(jsonStr) as CloudCodeAssistResponseChunk;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const responseData = chunk.response;
|
||||
if (!responseData?.candidates) continue;
|
||||
|
||||
for (const candidate of responseData.candidates) {
|
||||
const parts = candidate.content?.parts;
|
||||
if (!parts) continue;
|
||||
for (const part of parts) {
|
||||
if (part.text) {
|
||||
textParts.push(part.text);
|
||||
}
|
||||
if (part.inlineData?.data) {
|
||||
await reader.cancel();
|
||||
return {
|
||||
image: {
|
||||
data: part.inlineData.data,
|
||||
mimeType: part.inlineData.mimeType || "image/png",
|
||||
},
|
||||
text: textParts,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
|
||||
throw new Error("No image data returned by the model");
|
||||
}
|
||||
|
||||
async function getCredentials(ctx: {
|
||||
modelRegistry: { getApiKeyForProvider: (provider: string) => Promise<string | undefined> };
|
||||
}): Promise<ParsedCredentials> {
|
||||
const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER);
|
||||
if (!apiKey) {
|
||||
throw new Error("Missing Google Antigravity OAuth credentials. Run /login for google-antigravity.");
|
||||
}
|
||||
return parseOAuthCredentials(apiKey);
|
||||
}
|
||||
|
||||
export default function antigravityImageGen(pi: ExtensionAPI) {
|
||||
pi.registerTool({
|
||||
name: "generate_image",
|
||||
label: "Generate image",
|
||||
description:
|
||||
"Generate an image via Google Antigravity image models. Returns the image as a tool result attachment. Optional saving via save=project|global|custom|none, or PI_IMAGE_SAVE_MODE/PI_IMAGE_SAVE_DIR.",
|
||||
parameters: TOOL_PARAMS,
|
||||
async execute(_toolCallId, params: ToolParams, signal, onUpdate, ctx) {
|
||||
const { accessToken, projectId } = await getCredentials(ctx);
|
||||
const model = params.model || DEFAULT_MODEL;
|
||||
const aspectRatio = params.aspectRatio || DEFAULT_ASPECT_RATIO;
|
||||
|
||||
const requestBody = buildRequest(params.prompt, model, projectId, aspectRatio);
|
||||
onUpdate?.({
|
||||
content: [{ type: "text", text: `Requesting image from ${PROVIDER}/${model}...` }],
|
||||
details: { provider: PROVIDER, model, aspectRatio },
|
||||
});
|
||||
|
||||
const response = await fetch(`${ANTIGRAVITY_ENDPOINT}/v1internal:streamGenerateContent?alt=sse`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
...ANTIGRAVITY_HEADERS,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Image request failed (${response.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
const parsed = await parseSseForImage(response, signal);
|
||||
const saveConfig = resolveSaveConfig(params, ctx.cwd);
|
||||
let savedPath: string | undefined;
|
||||
let saveError: string | undefined;
|
||||
if (saveConfig.mode !== "none" && saveConfig.outputDir) {
|
||||
try {
|
||||
savedPath = await saveImage(parsed.image.data, parsed.image.mimeType, saveConfig.outputDir);
|
||||
} catch (error) {
|
||||
saveError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
const summaryParts = [`Generated image via ${PROVIDER}/${model}.`, `Aspect ratio: ${aspectRatio}.`];
|
||||
if (savedPath) {
|
||||
summaryParts.push(`Saved image to: ${savedPath}`);
|
||||
} else if (saveError) {
|
||||
summaryParts.push(`Failed to save image: ${saveError}`);
|
||||
}
|
||||
if (parsed.text.length > 0) {
|
||||
summaryParts.push(`Model notes: ${parsed.text.join(" ")}`);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: summaryParts.join(" ") },
|
||||
{ type: "image", data: parsed.image.data, mimeType: parsed.image.mimeType },
|
||||
],
|
||||
details: { provider: PROVIDER, model, aspectRatio, savedPath, saveMode: saveConfig.mode },
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -329,7 +329,6 @@ ${chalk.bold("Environment Variables:")}
|
||||
PI_OFFLINE - Disable startup network operations when set to 1/true/yes
|
||||
PI_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no
|
||||
PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)
|
||||
PI_AI_ANTIGRAVITY_VERSION - Override Antigravity User-Agent version (e.g., 1.23.0)
|
||||
|
||||
${chalk.bold("Built-in Tool Names:")}
|
||||
read - Read file contents
|
||||
|
||||
@@ -19,8 +19,6 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
||||
"openai-codex": "gpt-5.5",
|
||||
deepseek: "deepseek-v4-pro",
|
||||
google: "gemini-3.1-pro-preview",
|
||||
"google-gemini-cli": "gemini-3.1-pro-preview",
|
||||
"google-antigravity": "gemini-3.1-pro-high",
|
||||
"google-vertex": "gemini-3.1-pro-preview",
|
||||
"github-copilot": "gpt-5.4",
|
||||
openrouter: "moonshotai/kimi-k2.6",
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
/**
|
||||
* Test for compaction with thinking models.
|
||||
*
|
||||
* Tests both:
|
||||
* - Claude via Antigravity (google-gemini-cli API)
|
||||
* - Claude via real Anthropic API (anthropic-messages API)
|
||||
*
|
||||
* Reproduces issue where compact fails when maxTokens < thinkingBudget.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Agent, type ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||
import { getModel, type Model } from "@mariozechner/pi-ai";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { AgentSession } from "../src/core/agent-session.js";
|
||||
import { ModelRegistry } from "../src/core/model-registry.js";
|
||||
import { SessionManager } from "../src/core/session-manager.js";
|
||||
import { SettingsManager } from "../src/core/settings-manager.js";
|
||||
import { createCodingTools } from "../src/core/tools/index.js";
|
||||
import {
|
||||
API_KEY,
|
||||
createTestResourceLoader,
|
||||
getRealAuthStorage,
|
||||
hasAuthForProvider,
|
||||
resolveApiKey,
|
||||
} from "./utilities.js";
|
||||
|
||||
// Check for auth
|
||||
const HAS_ANTIGRAVITY_AUTH = hasAuthForProvider("google-antigravity");
|
||||
const HAS_ANTHROPIC_AUTH = !!API_KEY;
|
||||
|
||||
describe.skipIf(!HAS_ANTIGRAVITY_AUTH)("Compaction with thinking models (Antigravity)", () => {
|
||||
let session: AgentSession;
|
||||
let tempDir: string;
|
||||
let apiKey: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const key = await resolveApiKey("google-antigravity");
|
||||
if (!key) throw new Error("Failed to resolve google-antigravity API key");
|
||||
apiKey = key;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = join(tmpdir(), `pi-thinking-compaction-test-${Date.now()}`);
|
||||
mkdirSync(tempDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (session) {
|
||||
session.dispose();
|
||||
}
|
||||
if (tempDir && existsSync(tempDir)) {
|
||||
rmSync(tempDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
function createSession(
|
||||
modelId: "claude-opus-4-5-thinking" | "claude-sonnet-4-5",
|
||||
thinkingLevel: ThinkingLevel = "high",
|
||||
) {
|
||||
const model = getModel("google-antigravity", modelId);
|
||||
if (!model) {
|
||||
throw new Error(`Model not found: google-antigravity/${modelId}`);
|
||||
}
|
||||
|
||||
const agent = new Agent({
|
||||
getApiKey: () => apiKey,
|
||||
initialState: {
|
||||
model,
|
||||
systemPrompt: "You are a helpful assistant. Be concise.",
|
||||
tools: createCodingTools(process.cwd()),
|
||||
thinkingLevel,
|
||||
},
|
||||
});
|
||||
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const settingsManager = SettingsManager.create(tempDir, tempDir);
|
||||
// Use minimal keepRecentTokens so small test conversations have something to summarize
|
||||
// settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } });
|
||||
|
||||
const authStorage = getRealAuthStorage();
|
||||
const modelRegistry = ModelRegistry.create(authStorage);
|
||||
|
||||
session = new AgentSession({
|
||||
agent,
|
||||
sessionManager,
|
||||
settingsManager,
|
||||
cwd: tempDir,
|
||||
modelRegistry,
|
||||
resourceLoader: createTestResourceLoader(),
|
||||
});
|
||||
|
||||
session.subscribe(() => {});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
it("should compact successfully with claude-opus-4-5-thinking and thinking level high", async () => {
|
||||
createSession("claude-opus-4-5-thinking", "high");
|
||||
|
||||
// Send a simple prompt
|
||||
await session.prompt("Write down the first 10 prime numbers.");
|
||||
await session.agent.waitForIdle();
|
||||
|
||||
// Verify we got a response
|
||||
const messages = session.messages;
|
||||
expect(messages.length).toBeGreaterThan(0);
|
||||
|
||||
const assistantMessages = messages.filter((m) => m.role === "assistant");
|
||||
expect(assistantMessages.length).toBeGreaterThan(0);
|
||||
|
||||
// Now try to compact - this should not throw
|
||||
const result = await session.compact();
|
||||
|
||||
expect(result.summary).toBeDefined();
|
||||
expect(result.summary.length).toBeGreaterThan(0);
|
||||
expect(result.tokensBefore).toBeGreaterThan(0);
|
||||
|
||||
// Verify session is still usable after compaction
|
||||
const messagesAfterCompact = session.messages;
|
||||
expect(messagesAfterCompact.length).toBeGreaterThan(0);
|
||||
expect(messagesAfterCompact[0].role).toBe("compactionSummary");
|
||||
}, 180000);
|
||||
|
||||
it("should compact successfully with claude-sonnet-4-5 (non-thinking) for comparison", async () => {
|
||||
createSession("claude-sonnet-4-5", "off");
|
||||
|
||||
await session.prompt("Write down the first 10 prime numbers.");
|
||||
await session.agent.waitForIdle();
|
||||
|
||||
const messages = session.messages;
|
||||
expect(messages.length).toBeGreaterThan(0);
|
||||
|
||||
const result = await session.compact();
|
||||
|
||||
expect(result.summary).toBeDefined();
|
||||
expect(result.summary.length).toBeGreaterThan(0);
|
||||
}, 180000);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Real Anthropic API tests (for comparison)
|
||||
// ============================================================================
|
||||
|
||||
describe.skipIf(!HAS_ANTHROPIC_AUTH)("Compaction with thinking models (Anthropic)", () => {
|
||||
let session: AgentSession;
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = join(tmpdir(), `pi-thinking-compaction-anthropic-test-${Date.now()}`);
|
||||
mkdirSync(tempDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (session) {
|
||||
session.dispose();
|
||||
}
|
||||
if (tempDir && existsSync(tempDir)) {
|
||||
rmSync(tempDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
function createSession(model: Model<any>, thinkingLevel: ThinkingLevel = "high") {
|
||||
const agent = new Agent({
|
||||
getApiKey: () => API_KEY,
|
||||
initialState: {
|
||||
model,
|
||||
systemPrompt: "You are a helpful assistant. Be concise.",
|
||||
tools: createCodingTools(process.cwd()),
|
||||
thinkingLevel,
|
||||
},
|
||||
});
|
||||
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const settingsManager = SettingsManager.create(tempDir, tempDir);
|
||||
|
||||
const authStorage = getRealAuthStorage();
|
||||
const modelRegistry = ModelRegistry.create(authStorage);
|
||||
|
||||
session = new AgentSession({
|
||||
agent,
|
||||
sessionManager,
|
||||
settingsManager,
|
||||
cwd: tempDir,
|
||||
modelRegistry,
|
||||
resourceLoader: createTestResourceLoader(),
|
||||
});
|
||||
|
||||
session.subscribe(() => {});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
it("should compact successfully with claude-sonnet-4-5 and thinking level high", async () => {
|
||||
const model = getModel("anthropic", "claude-sonnet-4-5")!;
|
||||
createSession(model, "high");
|
||||
|
||||
// Send a simple prompt
|
||||
await session.prompt("Write down the first 10 prime numbers.");
|
||||
await session.agent.waitForIdle();
|
||||
|
||||
// Verify we got a response
|
||||
const messages = session.messages;
|
||||
expect(messages.length).toBeGreaterThan(0);
|
||||
|
||||
const assistantMessages = messages.filter((m) => m.role === "assistant");
|
||||
expect(assistantMessages.length).toBeGreaterThan(0);
|
||||
|
||||
// Now try to compact - this should not throw
|
||||
const result = await session.compact();
|
||||
|
||||
expect(result.summary).toBeDefined();
|
||||
expect(result.summary.length).toBeGreaterThan(0);
|
||||
expect(result.tokensBefore).toBeGreaterThan(0);
|
||||
|
||||
// Verify session is still usable after compaction
|
||||
const messagesAfterCompact = session.messages;
|
||||
expect(messagesAfterCompact.length).toBeGreaterThan(0);
|
||||
expect(messagesAfterCompact[0].role).toBe("compactionSummary");
|
||||
}, 180000);
|
||||
});
|
||||
@@ -71,7 +71,6 @@ function saveAuthStorage(storage: AuthStorageData): void {
|
||||
* For API key credentials, returns the key directly.
|
||||
* For OAuth credentials, returns the access token (refreshing if expired and saving back).
|
||||
*
|
||||
* For google-gemini-cli and google-antigravity, returns JSON-encoded { token, projectId }
|
||||
*/
|
||||
export async function resolveApiKey(provider: string): Promise<string | undefined> {
|
||||
const storage = loadAuthStorage();
|
||||
|
||||
Reference in New Issue
Block a user