paseo-acp-agy 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.
@@ -0,0 +1,471 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { fileURLToPath } from "node:url";
4
+ import { logger } from "./logger.js";
5
+ import { saveBase64Image } from "./attachments.js";
6
+ const execFileAsync = promisify(execFile);
7
+ // ACP wire methods use snake_case. The aliases retain the public method names
8
+ // accepted by the adapter before the hardening work so existing clients do not
9
+ // lose compatibility.
10
+ export const ACP_METHODS = {
11
+ INITIALIZE: "initialize",
12
+ SESSION_NEW: "session/new",
13
+ SESSION_NEW_ALIAS: "newSession",
14
+ SESSION_LOAD: "session/load",
15
+ SESSION_LOAD_ALIAS: "loadSession",
16
+ SESSION_RESUME: "session/resume",
17
+ SESSION_RESUME_ALIAS: "unstable_resumeSession",
18
+ SESSION_PROMPT: "session/prompt",
19
+ SESSION_PROMPT_ALIAS: "prompt",
20
+ SESSION_CANCEL: "session/cancel",
21
+ SESSION_CANCEL_ALIAS: "cancel",
22
+ SESSION_CLOSE: "session/close",
23
+ SESSION_CLOSE_ALIAS: "unstable_closeSession",
24
+ SESSION_SET_MODE: "session/set_mode",
25
+ SESSION_SET_MODE_ALIAS: "setSessionMode",
26
+ SESSION_SET_MODEL: "session/set_model",
27
+ SESSION_SET_MODEL_ALIAS: "unstable_setSessionModel",
28
+ SESSION_SET_CONFIG_OPTION: "session/set_config_option",
29
+ SESSION_SET_CONFIG_OPTION_ALIAS: "setSessionConfigOption",
30
+ SESSION_UPDATE: "session/update",
31
+ };
32
+ export function mapToolNameToKind(toolName) {
33
+ const name = toolName.toLowerCase();
34
+ if (name.includes("read") ||
35
+ name.includes("view") ||
36
+ name.includes("list") ||
37
+ name.includes("grep") ||
38
+ name.includes("find") ||
39
+ name === "view_file" ||
40
+ name === "list_dir")
41
+ return "read";
42
+ if (name.includes("write") ||
43
+ name.includes("replace") ||
44
+ name.includes("edit") ||
45
+ name.includes("sed") ||
46
+ name === "write_to_file" ||
47
+ name === "replace_file_content" ||
48
+ name === "multi_replace_file_content")
49
+ return "edit";
50
+ if (name.includes("command") ||
51
+ name.includes("terminal") ||
52
+ name.includes("execute") ||
53
+ name === "run_command")
54
+ return "execute";
55
+ if (name.includes("search") ||
56
+ name.includes("url") ||
57
+ name.includes("browser") ||
58
+ name === "search_web")
59
+ return "fetch";
60
+ return "other";
61
+ }
62
+ export function extractPromptText(promptInput) {
63
+ if (typeof promptInput === "string") {
64
+ const trimmed = promptInput.trim();
65
+ return trimmed || "Please continue.";
66
+ }
67
+ const textParts = [];
68
+ const imagePaths = [];
69
+ const processBlock = (block) => {
70
+ if (!block)
71
+ return;
72
+ if (typeof block === "string") {
73
+ if (block.trim())
74
+ textParts.push(block.trim());
75
+ return;
76
+ }
77
+ if (typeof block !== "object")
78
+ return;
79
+ const b = block;
80
+ if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
81
+ textParts.push(b.text.trim());
82
+ return;
83
+ }
84
+ if (b.type === "image") {
85
+ if (typeof b.uri === "string" && b.uri.startsWith("file://")) {
86
+ try {
87
+ imagePaths.push(fileURLToPath(b.uri));
88
+ }
89
+ catch {
90
+ logger.warn("Ignoring invalid file:// image URI");
91
+ }
92
+ }
93
+ else if (typeof b.path === "string" && b.path.trim()) {
94
+ imagePaths.push(b.path.trim());
95
+ }
96
+ else if (typeof b.data === "string" && b.data.length > 0) {
97
+ const mime = typeof b.mimeType === "string" ? b.mimeType : "image/png";
98
+ const savedPath = saveBase64Image(b.data, mime);
99
+ if (savedPath)
100
+ imagePaths.push(savedPath);
101
+ }
102
+ return;
103
+ }
104
+ if (b.type === "resource_link" && typeof b.uri === "string") {
105
+ textParts.push(`[Resource: ${b.uri}]`);
106
+ }
107
+ else if (typeof b.content === "string" && b.content.trim()) {
108
+ textParts.push(b.content.trim());
109
+ }
110
+ };
111
+ if (Array.isArray(promptInput)) {
112
+ for (const item of promptInput)
113
+ processBlock(item);
114
+ }
115
+ else {
116
+ processBlock(promptInput);
117
+ }
118
+ const imageInstructions = imagePaths.map((imgPath) => `[Attached Image: ${imgPath}]\n(The user attached an image saved at "${imgPath}". Please inspect and view it using the \`view_file\` tool with AbsolutePath="${imgPath}".)`);
119
+ if (textParts.length > 0) {
120
+ if (imageInstructions.length > 0) {
121
+ return `${textParts.join("\n\n")}\n\n${imageInstructions.join("\n\n")}`;
122
+ }
123
+ return textParts.join("\n\n");
124
+ }
125
+ if (imageInstructions.length > 0) {
126
+ return `Please inspect and analyze the attached image(s) using the \`view_file\` tool.\n\n${imageInstructions.join("\n\n")}`;
127
+ }
128
+ return "Please continue.";
129
+ }
130
+ export const MODEL_PRICING = {
131
+ "gemini-3.8-flash": { inputCostPerM: 0.10, outputCostPerM: 0.40, cachedInputCostPerM: 0.025 },
132
+ "gemini-3.7-flash": { inputCostPerM: 0.10, outputCostPerM: 0.40, cachedInputCostPerM: 0.025 },
133
+ "gemini-3.6-flash": { inputCostPerM: 0.10, outputCostPerM: 0.40, cachedInputCostPerM: 0.025 },
134
+ "gemini-3.1-pro": { inputCostPerM: 1.25, outputCostPerM: 5.00, cachedInputCostPerM: 0.3125 },
135
+ "claude-sonnet-4-6": { inputCostPerM: 3.00, outputCostPerM: 15.00, cachedInputCostPerM: 0.30 },
136
+ "claude-opus-4-6-thinking": { inputCostPerM: 15.00, outputCostPerM: 75.00, cachedInputCostPerM: 1.50 },
137
+ "gpt-oss-120b": { inputCostPerM: 0.15, outputCostPerM: 0.60, cachedInputCostPerM: 0.0375 },
138
+ };
139
+ export const MODEL_CONTEXT_WINDOWS = {
140
+ "gemini-3.8-flash": 1_048_576,
141
+ "gemini-3.7-flash": 1_048_576,
142
+ "gemini-3.6-flash": 1_048_576,
143
+ "gemini-3.1-pro": 2_097_152,
144
+ "claude-sonnet-4-6": 200_000,
145
+ "claude-opus-4-6-thinking": 200_000,
146
+ "gpt-oss-120b": 128_000,
147
+ };
148
+ export function getModelContextWindow(modelId) {
149
+ if (!modelId)
150
+ return 1_048_576;
151
+ if (MODEL_CONTEXT_WINDOWS[modelId])
152
+ return MODEL_CONTEXT_WINDOWS[modelId];
153
+ const lower = modelId.toLowerCase();
154
+ if (lower.includes("pro"))
155
+ return 2_097_152;
156
+ if (lower.includes("claude"))
157
+ return 200_000;
158
+ if (lower.includes("gpt"))
159
+ return 128_000;
160
+ if (lower.includes("flash"))
161
+ return 1_048_576;
162
+ return 1_048_576;
163
+ }
164
+ export function getModelPricing(modelId) {
165
+ if (!modelId)
166
+ return null;
167
+ if (MODEL_PRICING[modelId])
168
+ return MODEL_PRICING[modelId];
169
+ const lower = modelId.toLowerCase();
170
+ if (lower.includes("opus"))
171
+ return MODEL_PRICING["claude-opus-4-6-thinking"];
172
+ if (lower.includes("claude") || lower.includes("sonnet"))
173
+ return MODEL_PRICING["claude-sonnet-4-6"];
174
+ if (lower.includes("pro"))
175
+ return MODEL_PRICING["gemini-3.1-pro"];
176
+ if (lower.includes("flash"))
177
+ return MODEL_PRICING["gemini-3.7-flash"];
178
+ if (lower.includes("gpt-oss"))
179
+ return MODEL_PRICING["gpt-oss-120b"];
180
+ return null;
181
+ }
182
+ // Keep costs at full JavaScript floating-point precision. Rounding happens
183
+ // only when values are serialized for the UI so micro-costs accumulate.
184
+ export function calculateUsageCostUsd(modelId = "gemini-3.7-flash", inputTokens = 0, outputTokens = 0, cachedInputTokens = 0) {
185
+ const pricing = getModelPricing(modelId);
186
+ if (!pricing)
187
+ return 0;
188
+ const inputCost = (inputTokens / 1_000_000) * pricing.inputCostPerM;
189
+ const outputCost = (outputTokens / 1_000_000) * pricing.outputCostPerM;
190
+ const cachedCost = (cachedInputTokens / 1_000_000) * pricing.cachedInputCostPerM;
191
+ return inputCost + outputCost + cachedCost;
192
+ }
193
+ export function roundUsageCostUsd(costUsd, decimals = 6) {
194
+ const factor = 10 ** decimals;
195
+ return Math.round(costUsd * factor) / factor;
196
+ }
197
+ export function deriveUsageTone(usedPct) {
198
+ if (usedPct >= 90)
199
+ return "danger";
200
+ if (usedPct >= 75)
201
+ return "warning";
202
+ return "ok";
203
+ }
204
+ export function parseAgyQuotaOutput(raw) {
205
+ const windows = [];
206
+ const lines = raw.split(/[\r\n]+/);
207
+ for (const line of lines) {
208
+ const trimmed = line.trim();
209
+ if (!trimmed || trimmed.toLowerCase().startsWith("quota:"))
210
+ continue;
211
+ const parts = trimmed.split(/\t+|\s{2,}/).map((p) => p.trim());
212
+ if (parts.length >= 3) {
213
+ const scope = parts[0];
214
+ const limitType = parts[1];
215
+ const remainingMatch = parts[2].match(/(\d+)%/);
216
+ const resetsAt = parts[3] || null;
217
+ if (remainingMatch) {
218
+ const remainingPct = parseInt(remainingMatch[1], 10);
219
+ const usedPct = Math.max(0, Math.min(100, 100 - remainingPct));
220
+ const isFiveHour = /five\s*hour/i.test(limitType);
221
+ const isWeekly = /weekly/i.test(limitType);
222
+ const isGemini = /gemini/i.test(scope);
223
+ let id = isFiveHour ? "session" : isWeekly ? "weekly" : "quota";
224
+ let label = isFiveHour ? "Session (5h)" : isWeekly ? "Weekly" : limitType;
225
+ if (!isGemini) {
226
+ id = `claude_${id}`;
227
+ label = `Claude ${label}`;
228
+ }
229
+ windows.push({
230
+ id,
231
+ label,
232
+ usedPct,
233
+ remainingPct,
234
+ resetsAt,
235
+ tone: deriveUsageTone(usedPct),
236
+ });
237
+ }
238
+ }
239
+ }
240
+ return windows;
241
+ }
242
+ export function parseAgyCreditsOutput(raw) {
243
+ const match = raw.match(/Remaining\s+credits\s+([\d.]+)/i);
244
+ const remaining = match ? parseFloat(match[1]) : 0;
245
+ return {
246
+ id: "credits",
247
+ label: "Credits",
248
+ remaining,
249
+ unit: "usd",
250
+ tone: remaining > 0 ? "ok" : "default",
251
+ };
252
+ }
253
+ let cachedProviderUsage = null;
254
+ let cachedProviderUsageBinaryPath = null;
255
+ let lastProviderUsageFetch = 0;
256
+ const PROVIDER_USAGE_CACHE_TTL_MS = 30_000;
257
+ function errorMessage(reason) {
258
+ if (reason instanceof Error)
259
+ return reason.message;
260
+ return String(reason || "unknown error");
261
+ }
262
+ function cacheProviderUsage(binaryPath, result, now) {
263
+ cachedProviderUsage = result;
264
+ cachedProviderUsageBinaryPath = binaryPath;
265
+ lastProviderUsageFetch = now;
266
+ return result;
267
+ }
268
+ export async function fetchAntigravityUsage(binaryPath = "agy", force = false) {
269
+ const now = Date.now();
270
+ if (!force &&
271
+ cachedProviderUsage &&
272
+ cachedProviderUsageBinaryPath === binaryPath &&
273
+ now - lastProviderUsageFetch < PROVIDER_USAGE_CACHE_TTL_MS) {
274
+ return cachedProviderUsage;
275
+ }
276
+ try {
277
+ const [usageResult, creditsResult] = await Promise.allSettled([
278
+ execFileAsync(binaryPath, ["--print", "/usage"], {
279
+ timeout: 8_000,
280
+ maxBuffer: 1024 * 1024,
281
+ env: process.env,
282
+ }),
283
+ execFileAsync(binaryPath, ["--print", "/credits"], {
284
+ timeout: 8_000,
285
+ maxBuffer: 1024 * 1024,
286
+ env: process.env,
287
+ }),
288
+ ]);
289
+ const balances = [];
290
+ if (creditsResult.status === "fulfilled") {
291
+ const creditsOutput = creditsResult.value.stdout || creditsResult.value.stderr || "";
292
+ if (creditsOutput.trim())
293
+ balances.push(parseAgyCreditsOutput(creditsOutput));
294
+ }
295
+ // /usage is the primary provider-availability probe. allSettled never
296
+ // throws for an exec failure, so rejected results must be handled here.
297
+ if (usageResult.status === "rejected") {
298
+ const error = `Unable to fetch Antigravity quota: ${errorMessage(usageResult.reason)}`;
299
+ logger.warn("Failed to fetch Antigravity provider usage", { error });
300
+ return cacheProviderUsage(binaryPath, {
301
+ providerId: "antigravity",
302
+ displayName: "Antigravity",
303
+ status: "unavailable",
304
+ planLabel: null,
305
+ windows: [],
306
+ balances,
307
+ details: [],
308
+ error,
309
+ }, now);
310
+ }
311
+ const quotaOutput = usageResult.value.stdout || usageResult.value.stderr || "";
312
+ const windows = parseAgyQuotaOutput(quotaOutput);
313
+ const partialError = creditsResult.status === "rejected"
314
+ ? `Credits unavailable: ${errorMessage(creditsResult.reason)}`
315
+ : null;
316
+ return cacheProviderUsage(binaryPath, {
317
+ providerId: "antigravity",
318
+ displayName: "Antigravity",
319
+ status: "available",
320
+ planLabel: "Google Gemini",
321
+ windows,
322
+ balances,
323
+ details: [],
324
+ error: partialError,
325
+ }, now);
326
+ }
327
+ catch (err) {
328
+ const error = errorMessage(err);
329
+ logger.warn("Failed to fetch Antigravity provider usage", { error });
330
+ return cacheProviderUsage(binaryPath, {
331
+ providerId: "antigravity",
332
+ displayName: "Antigravity",
333
+ status: "unavailable",
334
+ planLabel: null,
335
+ windows: [],
336
+ balances: [],
337
+ details: [],
338
+ error,
339
+ }, now);
340
+ }
341
+ }
342
+ export const ALL_THINKING_LEVELS = {
343
+ low: { name: "Low", description: "Fast response with minimal reasoning latency" },
344
+ medium: { name: "Medium", description: "Balanced reasoning effort and speed" },
345
+ high: { name: "High", description: "Deep reasoning effort for complex tasks" },
346
+ };
347
+ export const FALLBACK_MODELS = [
348
+ { modelId: "gemini-3.8-flash", name: "Gemini 3.8 Flash", description: "Google Gemini 3.8 Flash", supportedEfforts: ["high", "medium", "low"], contextWindowMaxTokens: 1_048_576 },
349
+ { modelId: "gemini-3.7-flash", name: "Gemini 3.7 Flash", description: "Google Gemini 3.7 Flash", supportedEfforts: ["high", "medium", "low"], contextWindowMaxTokens: 1_048_576 },
350
+ { modelId: "gemini-3.6-flash", name: "Gemini 3.6 Flash", description: "Google Gemini 3.6 Flash", supportedEfforts: ["high", "medium", "low"], contextWindowMaxTokens: 1_048_576 },
351
+ { modelId: "gemini-3.1-pro", name: "Gemini 3.1 Pro", description: "Google Gemini 3.1 Pro", supportedEfforts: ["high", "low"], contextWindowMaxTokens: 2_097_152 },
352
+ { modelId: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Thinking)", description: "Anthropic Claude Sonnet 4.6 (Thinking)", supportedEfforts: [], contextWindowMaxTokens: 200_000 },
353
+ { modelId: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 (Thinking)", description: "Anthropic Claude Opus 4.6 (Thinking)", supportedEfforts: [], contextWindowMaxTokens: 200_000 },
354
+ { modelId: "gpt-oss-120b", name: "GPT-OSS 120B", description: "GPT-OSS 120B (Medium)", supportedEfforts: ["medium"], contextWindowMaxTokens: 128_000 },
355
+ ];
356
+ let cachedModels = null;
357
+ let cachedModelsBinaryPath = null;
358
+ let lastModelFetch = 0;
359
+ const MODEL_CACHE_TTL_MS = 60_000;
360
+ export function parseAgyModelsOutput(rawOutput) {
361
+ const modelsMap = new Map();
362
+ for (const line of rawOutput.split(/[\r\n]+/)) {
363
+ const trimmed = line.trim();
364
+ if (!trimmed || trimmed.includes("Fetching available models"))
365
+ continue;
366
+ const parts = trimmed.split(/\t+|\s{2,}/).map((p) => p.trim()).filter(Boolean);
367
+ if (parts.length < 2)
368
+ continue;
369
+ const modelId = parts[0];
370
+ const label = parts[1];
371
+ const effortMatch = modelId.match(/-(high|medium|low)$/);
372
+ let baseId = modelId;
373
+ let baseLabel = label;
374
+ let effort = null;
375
+ if (effortMatch) {
376
+ effort = effortMatch[1];
377
+ baseId = modelId.slice(0, -(effort.length + 1));
378
+ baseLabel = label.replace(/\s*\((High|Medium|Low)\)$/, "");
379
+ }
380
+ if (!modelsMap.has(baseId)) {
381
+ modelsMap.set(baseId, {
382
+ modelId: baseId,
383
+ name: baseLabel,
384
+ description: label,
385
+ supportedEfforts: [],
386
+ contextWindowMaxTokens: getModelContextWindow(baseId),
387
+ });
388
+ }
389
+ if (effort) {
390
+ const entry = modelsMap.get(baseId);
391
+ if (!entry.supportedEfforts.includes(effort))
392
+ entry.supportedEfforts.push(effort);
393
+ }
394
+ }
395
+ const result = Array.from(modelsMap.values());
396
+ return result.length > 0 ? result : FALLBACK_MODELS;
397
+ }
398
+ export async function fetchAvailableModels(binaryPath = "agy", force = false) {
399
+ const now = Date.now();
400
+ if (!force &&
401
+ cachedModels &&
402
+ cachedModelsBinaryPath === binaryPath &&
403
+ now - lastModelFetch < MODEL_CACHE_TTL_MS) {
404
+ return cachedModels;
405
+ }
406
+ try {
407
+ const { stdout } = await execFileAsync(binaryPath, ["models"], {
408
+ timeout: 5_000,
409
+ env: process.env,
410
+ maxBuffer: 4 * 1024 * 1024,
411
+ });
412
+ const parsed = parseAgyModelsOutput(stdout);
413
+ cachedModels = parsed;
414
+ cachedModelsBinaryPath = binaryPath;
415
+ lastModelFetch = now;
416
+ return parsed;
417
+ }
418
+ catch (err) {
419
+ logger.warn("Failed to fetch models from agy CLI, using fallback models", {
420
+ error: err.message,
421
+ });
422
+ cachedModels = FALLBACK_MODELS;
423
+ cachedModelsBinaryPath = binaryPath;
424
+ lastModelFetch = now;
425
+ return FALLBACK_MODELS;
426
+ }
427
+ }
428
+ export function getEffectiveEffortForModel(modelId, requestedEffort, models) {
429
+ if (!modelId)
430
+ return undefined;
431
+ const modelList = models || cachedModels || FALLBACK_MODELS;
432
+ const modelDef = modelList.find((m) => m.modelId === modelId);
433
+ if (!modelDef || modelDef.supportedEfforts.length === 0)
434
+ return undefined;
435
+ if (requestedEffort && modelDef.supportedEfforts.includes(requestedEffort))
436
+ return requestedEffort;
437
+ if (modelDef.supportedEfforts.includes("high"))
438
+ return "high";
439
+ return modelDef.supportedEfforts[0];
440
+ }
441
+ export const AVAILABLE_MODES = [
442
+ { id: "default", name: "Default (Accept Edits)", description: "Standard autonomous execution and coding mode" },
443
+ { id: "plan", name: "Plan Mode", description: "Planning and read-only analysis without file modifications" },
444
+ ];
445
+ export function buildConfigOptionsForModel(modelId, currentEffort = "high", availableModels) {
446
+ const modelList = availableModels || cachedModels || FALLBACK_MODELS;
447
+ const modelDef = modelList.find((m) => m.modelId === modelId);
448
+ const supported = modelDef ? modelDef.supportedEfforts : ["high", "medium", "low"];
449
+ if (!supported || supported.length === 0)
450
+ return [];
451
+ const effectiveEffort = supported.includes(currentEffort)
452
+ ? currentEffort
453
+ : supported.includes("high")
454
+ ? "high"
455
+ : supported[0];
456
+ const options = supported.map((effortId) => ({
457
+ value: effortId,
458
+ name: ALL_THINKING_LEVELS[effortId]?.name || effortId.toUpperCase(),
459
+ description: ALL_THINKING_LEVELS[effortId]?.description || `${effortId} effort`,
460
+ }));
461
+ return [
462
+ {
463
+ id: "thought_level",
464
+ name: "Thinking Effort",
465
+ category: "thought_level",
466
+ type: "select",
467
+ currentValue: effectiveEffort,
468
+ options,
469
+ },
470
+ ];
471
+ }
@@ -0,0 +1,27 @@
1
+ export interface SessionUsageState {
2
+ inputTokens: number;
3
+ outputTokens: number;
4
+ cachedInputTokens: number;
5
+ totalTokens: number;
6
+ totalCostUsd: number;
7
+ contextWindowUsedTokens: number;
8
+ contextWindowMaxTokens: number;
9
+ }
10
+ export interface PersistedSessionState {
11
+ sessionId: string;
12
+ conversationId?: string;
13
+ cwd: string;
14
+ model: string;
15
+ effort: string;
16
+ mode: string;
17
+ usage?: SessionUsageState;
18
+ updatedAt: string;
19
+ }
20
+ export declare class SessionStore {
21
+ private dir;
22
+ constructor(dir?: string);
23
+ private pathFor;
24
+ load(sessionId: string): PersistedSessionState | null;
25
+ save(state: PersistedSessionState): void;
26
+ }
27
+ export declare const sessionStore: SessionStore;
@@ -0,0 +1,74 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import crypto from "node:crypto";
5
+ import { logger } from "./logger.js";
6
+ function getStateRoot() {
7
+ if (process.env.AGY_ACP_STATE_DIR) {
8
+ return process.env.AGY_ACP_STATE_DIR;
9
+ }
10
+ const stateHome = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
11
+ return path.join(stateHome, "agy-acp");
12
+ }
13
+ function fileNameForSession(sessionId) {
14
+ return crypto.createHash("sha256").update(sessionId).digest("hex") + ".json";
15
+ }
16
+ export class SessionStore {
17
+ dir;
18
+ constructor(dir) {
19
+ this.dir = dir || path.join(getStateRoot(), "sessions");
20
+ try {
21
+ fs.mkdirSync(this.dir, { recursive: true, mode: 0o700 });
22
+ fs.chmodSync(this.dir, 0o700);
23
+ }
24
+ catch { }
25
+ }
26
+ pathFor(sessionId) {
27
+ return path.join(this.dir, fileNameForSession(sessionId));
28
+ }
29
+ load(sessionId) {
30
+ const filePath = this.pathFor(sessionId);
31
+ if (!fs.existsSync(filePath)) {
32
+ return null;
33
+ }
34
+ try {
35
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
36
+ if (parsed.sessionId !== sessionId) {
37
+ logger.warn("Ignoring persisted session with mismatched session ID", { sessionId });
38
+ return null;
39
+ }
40
+ return parsed;
41
+ }
42
+ catch (err) {
43
+ logger.warn("Failed to load persisted ACP session", {
44
+ sessionId,
45
+ error: err.message,
46
+ });
47
+ return null;
48
+ }
49
+ }
50
+ save(state) {
51
+ const filePath = this.pathFor(state.sessionId);
52
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
53
+ try {
54
+ fs.writeFileSync(tempPath, JSON.stringify(state, null, 2) + "\n", {
55
+ encoding: "utf8",
56
+ mode: 0o600,
57
+ flag: "wx",
58
+ });
59
+ fs.renameSync(tempPath, filePath);
60
+ fs.chmodSync(filePath, 0o600);
61
+ }
62
+ catch (err) {
63
+ try {
64
+ fs.rmSync(tempPath, { force: true });
65
+ }
66
+ catch { }
67
+ logger.warn("Failed to persist ACP session state", {
68
+ sessionId: state.sessionId,
69
+ error: err.message,
70
+ });
71
+ }
72
+ }
73
+ }
74
+ export const sessionStore = new SessionStore();
@@ -0,0 +1,66 @@
1
+ import { AntigravityProcess } from "./antigravity-process.js";
2
+ import { SessionStore, SessionUsageState } from "./session-store.js";
3
+ export interface SessionOptions {
4
+ id?: string;
5
+ cwd?: string;
6
+ model?: string;
7
+ effort?: string;
8
+ mode?: string;
9
+ conversationId?: string;
10
+ usage?: SessionUsageState;
11
+ sandbox?: boolean;
12
+ dangerouslySkipPermissions?: boolean;
13
+ env?: Record<string, string>;
14
+ binaryPath?: string;
15
+ store?: SessionStore;
16
+ }
17
+ export declare class Session {
18
+ readonly id: string;
19
+ cwd: string;
20
+ model: string;
21
+ effort: string;
22
+ mode: string;
23
+ readonly createdAt: Date;
24
+ lastActivity: Date;
25
+ isCancelled: boolean;
26
+ process: AntigravityProcess;
27
+ usage: SessionUsageState;
28
+ private store;
29
+ private promptOperationActive;
30
+ constructor(options?: SessionOptions);
31
+ private persistedState;
32
+ persist(): void;
33
+ get isPromptOperationActive(): boolean;
34
+ tryBeginPromptOperation(): boolean;
35
+ endPromptOperation(): void;
36
+ setModel(model: string): void;
37
+ setEffort(effort: string): void;
38
+ setMode(mode: string): void;
39
+ setConversationId(conversationId?: string): void;
40
+ ensureReadyForResume(): Promise<void>;
41
+ resumeConversation(conversationId: string): Promise<void>;
42
+ recordTurnUsage(turnUsage?: {
43
+ input_tokens?: number;
44
+ output_tokens?: number;
45
+ cache_read_tokens?: number;
46
+ total_tokens?: number;
47
+ }, executingModel?: string): void;
48
+ touch(): void;
49
+ cancelTurn(): boolean;
50
+ close(): Promise<void>;
51
+ }
52
+ export declare class SessionManager {
53
+ private sessions;
54
+ private defaultBinaryPath?;
55
+ private store;
56
+ constructor(options?: {
57
+ defaultBinaryPath?: string;
58
+ store?: SessionStore;
59
+ });
60
+ createSession(options?: SessionOptions): Session;
61
+ getSession(id: string): Session | undefined;
62
+ hasPersistedSession(id: string): boolean;
63
+ closeSession(id: string): Promise<boolean>;
64
+ closeAll(): Promise<void>;
65
+ listSessions(): Session[];
66
+ }